screen-recorder 1.3.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,19 +1,16 @@
1
1
  # ScreenRecorder
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/screen-recorder.svg)](https://badge.fury.io/rb/screen-recorder)
4
- [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/github/kapoorlakshya/screen-recorder/master)
5
- [![Build Status](https://travis-ci.org/kapoorlakshya/screen-recorder.svg?branch=master)](https://travis-ci.org/kapoorlakshya/screen-recorder)
6
- [![AppVeyor status](https://ci.appveyor.com/api/projects/status/u1qashueuw82r235/branch/master?svg=true)](https://ci.appveyor.com/project/kapoorlakshya/screen-recorder/branch/master)
7
- [![Maintainability](https://api.codeclimate.com/v1/badges/b6049dfee7375aed9bc8/maintainability)](https://codeclimate.com/github/kapoorlakshya/screen-recorder/maintainability)
8
- [![Test Coverage](https://api.codeclimate.com/v1/badges/b6049dfee7375aed9bc8/test_coverage)](https://codeclimate.com/github/kapoorlakshya/screen-recorder/test_coverage)
4
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/gems/screen-recorder/)
5
+ [![Tests](https://github.com/kapoorlakshya/screen-recorder/actions/workflows/tests.yml/badge.svg)](https://github.com/kapoorlakshya/screen-recorder/actions/workflows/tests.yml)
9
6
 
10
- A Ruby gem to video record your computer screen - desktop or specific
7
+ A Ruby gem to video record or take screenshots of your computer screen - desktop or specific
11
8
  window - using [FFmpeg](https://www.ffmpeg.org/). Primarily
12
9
  geared towards recording automated UI (Selenium) test executions for
13
10
  debugging and documentation.
14
11
 
15
12
  #### Demo
16
- [https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem](https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem).
13
+ [https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem](https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem)
17
14
 
18
15
  ## Compatibility
19
16
 
@@ -23,11 +20,13 @@ Works on Windows, Linux, and macOS. Requires Ruby 2.0+ or JRuby 9.2+.
23
20
 
24
21
  ##### 1. Setup FFmpeg
25
22
 
26
- Linux and macOS instructions are [here](https://www.ffmpeg.org/download.html).
23
+ Linux and macOS instructions are [here](https://trac.ffmpeg.org/wiki/CompilationGuide).
27
24
 
28
- For Microsoft Windows, download the *libx264* enabled binary from [here](https://ffmpeg.zeranoe.com/builds/).
29
- Once downloaded, add location of the `ffmpeg/bin` folder to the `PATH`
30
- environment variable ([instructions](https://windowsloop.com/install-ffmpeg-windows-10/)).
25
+ > macOS: Follow [these steps](https://github.com/kapoorlakshya/screen-recorder/issues/88#issuecomment-629139032) to avoid
26
+ > issues related to Privacy settings.
27
+
28
+ For Microsoft Windows, download the binary from [here](https://www.videohelp.com/software/ffmpeg). Once downloaded,
29
+ add location of the `ffmpeg/bin` folder to the `PATH` environment variable ([instructions](https://windowsloop.com/install-ffmpeg-windows-10/)).
31
30
 
32
31
  Alternatively, you can point to the binary file using
33
32
  `ScreenRecorder.ffmpeg_binary = '/path/to/ffmpeg'` in your project.
@@ -38,13 +37,13 @@ Next, add these lines to your application's Gemfile:
38
37
 
39
38
  ```ruby
40
39
  gem 'ffi' # Windows only
41
- gem 'screen-recorder'
40
+ gem 'screen-recorder', '~> 1.0'
42
41
  ```
43
42
 
44
43
  The [`ffi`](https://github.com/ffi/ffi) gem is used by the
45
44
  [`childprocess`](https://github.com/enkessler/childprocess) gem on
46
45
  Windows, but it does not explicitly require it. More information
47
- on this [here](https://github.com/enkessler/childprocess/issues/150).
46
+ on this [here](https://github.com/enkessler/childprocess/issues/160).
48
47
 
49
48
 
50
49
  And then execute:
@@ -80,7 +79,7 @@ require 'screen-recorder'
80
79
  ```
81
80
 
82
81
  Linux and macOS users can optionally provide a display or input device number.
83
- Read more about it in the wiki [here](https://github.com/kapoorlakshya/screen-recorder/wiki/Input-Values).
82
+ Read more about it in the wiki [here](https://github.com/kapoorlakshya/screen-recorder/wiki/Display-or-Input-Device-Selection).
84
83
 
85
84
  #### Record Application Window (Microsoft Windows only)
86
85
 
@@ -96,7 +95,7 @@ browser = Watir::Browser.new :firefox
96
95
  @recorder.stop
97
96
  browser.quit
98
97
  ```
99
- This mode has limited capabilities. Read more about it in the wiki
98
+ This mode has a few limitations which are listed in the wiki
100
99
  [here](https://github.com/kapoorlakshya/screen-recorder/wiki/Window-Capture-Limitations).
101
100
 
102
101
  ##### Fetch Title
@@ -105,14 +104,76 @@ A helper method is available to fetch the title of the active window
105
104
  for the given process name.
106
105
 
107
106
  ```ruby
108
- ScreenRecorder::Window.fetch_title('firefox') # Name of exe
107
+ ScreenRecorder::('firefox') # Name of exe
109
108
  #=> ["Mozilla Firefox"]
110
109
 
111
- ScreenRecorder::Window.fetch_title('chrome')
110
+ ScreenRecorder::('chrome')
112
111
  #=> ["New Tab - Google Chrome"]
113
112
  ```
114
113
 
115
- #### Output
114
+ #### Capture Audio
115
+
116
+ Provide the following `advanced` configurations to capture audio:
117
+
118
+ ```ruby
119
+ # Linux
120
+ advanced = { f: 'alsa', ac: 2, i: 'hw:0'} # Using ALSA
121
+ # Or using PulseAudio
122
+ advanced = { 'f': 'pulse', 'ac': 2, 'i': 'default' } # Records default sound output device
123
+
124
+ # macOS
125
+ advanced = { input: { i: '1:1' } } # -i video:audio input device ID
126
+
127
+ # Windows
128
+ advanced = { f: 'dshow', i: 'audio="Microphone (Realtek High Definition Audio)"' }
129
+ ```
130
+
131
+ You can retrieve a list of audio devices by running these commands:
132
+
133
+ ```
134
+ # Linux
135
+ $ arecord -L # See https://trac.ffmpeg.org/wiki/Capture/ALSA
136
+
137
+ # macOS
138
+ $ ffmpeg -f avfoundation -list_devices true -i ""
139
+
140
+ # Windows
141
+ > ffmpeg -list_devices true -f dshow -i dummy
142
+ ```
143
+
144
+ #### Screenshots
145
+
146
+ Screenshots can be captured at any point after initializing the recorder:
147
+
148
+ ```ruby
149
+ # Desktop
150
+ @recorder = ScreenRecorder::Desktop.new(output: 'recording.mkv')
151
+ @recorder.screenshot('before-recording.png')
152
+ @recorder.start
153
+ @recorder.screenshot('during-recording.png')
154
+ @recorder.stop
155
+ @recorder.screenshot('after-recording.png')
156
+
157
+ # Window (Microsoft Windows only)
158
+ browser = Watir::Browser.new :chrome, options: { args: ['--disable-gpu'] } # Hardware acceleration must be disabled
159
+ browser.goto('watir.com')
160
+ window_title = ScreenRecorder::('chrome').first
161
+ @recorder = ScreenRecorder::Window.new(title: window_title, output: 'recording.mkv')
162
+ @recorder.screenshot('before-recording.png')
163
+ @recorder.start
164
+ @recorder.screenshot('during-recording.png')
165
+ @recorder.stop
166
+ @recorder.screenshot('after-recording.png')
167
+ browser.quit
168
+ ```
169
+
170
+ You can even specify a custom capture resolution:
171
+
172
+ ```rb
173
+ @recorder.screenshot('screenshot.png', '1024x768')
174
+ ```
175
+
176
+ #### Video Output
116
177
 
117
178
  Once the recorder is stopped, you can view the video metadata or transcode
118
179
  it if desired.
data/bin/console CHANGED
File without changes
data/bin/setup CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -12,14 +12,41 @@ module ScreenRecorder
12
12
  # ScreenRecorder.ffmpeg_binary = 'C:\ffmpeg.exe'
13
13
  #
14
14
  def self.ffmpeg_binary=(bin)
15
+ ScreenRecorder.logger.debug 'Setting ffmpeg path...'
15
16
  FFMPEG.ffmpeg_binary = bin
17
+ ScreenRecorder.logger.debug "ffmpeg path set: #{bin}"
18
+ ScreenRecorder.ffmpeg_binary
16
19
  end
17
20
 
18
21
  #
19
- # Returns path to ffmpeg binary
22
+ # Returns path to ffmpeg binary or raises DependencyNotFound
20
23
  #
21
24
  def self.ffmpeg_binary
22
25
  FFMPEG.ffmpeg_binary
26
+ rescue Errno::ENOENT # Raised when binary is not set in project or found in ENV
27
+ raise Errors::DependencyNotFound
28
+ end
29
+
30
+ #
31
+ # Uses user given ffprobe binary
32
+ #
33
+ # @example
34
+ # ScreenRecorder.ffprobe_binary= = 'C:\ffprobe.exe'
35
+ #
36
+ def self.ffprobe_binary=(bin)
37
+ ScreenRecorder.logger.debug 'Setting ffprobe path...'
38
+ FFMPEG.ffprobe_binary = bin
39
+ ScreenRecorder.logger.debug "ffprobe path set: #{bin}"
40
+ ScreenRecorder.ffmpeg_binary
41
+ end
42
+
43
+ #
44
+ # Returns path to ffprobe binary or raises DependencyNotFound
45
+ #
46
+ def self.ffprobe_binary
47
+ FFMPEG.ffprobe_binary
48
+ rescue Errno::ENOENT # Raised when binary is not set in project or found in ENV
49
+ raise Errors::DependencyNotFound
23
50
  end
24
51
 
25
52
  #
@@ -35,9 +62,9 @@ module ScreenRecorder
35
62
  def self.logger
36
63
  return @logger if @logger
37
64
 
38
- logger = Logger.new(STDOUT)
39
- logger.level = Logger::ERROR
40
- logger.progname = 'ScreenRecorder'
65
+ logger = Logger.new($stdout)
66
+ logger.level = Logger::ERROR
67
+ logger.progname = 'ScreenRecorder'
41
68
  logger.formatter = proc do |severity, time, progname, msg|
42
69
  "#{time.strftime('%F %T')} #{progname} - #{severity} - #{msg}\n"
43
70
  end
@@ -51,5 +78,6 @@ require 'screen-recorder/errors'
51
78
  require 'screen-recorder/options'
52
79
  require 'screen-recorder/titles'
53
80
  require 'screen-recorder/common'
81
+ require 'screen-recorder/screenshot'
54
82
  require 'screen-recorder/desktop'
55
83
  require 'screen-recorder/window'
@@ -9,6 +9,8 @@ module ScreenRecorder
9
9
  attr_reader :options, :video
10
10
 
11
11
  def initialize(input:, output:, advanced: {})
12
+ raise Errors::DependencyNotFound unless ffmpeg_exists?
13
+
12
14
  @options = Options.new(input: input, output: output, advanced: advanced)
13
15
  @video = nil
14
16
  @process = nil
@@ -19,8 +21,8 @@ module ScreenRecorder
19
21
  #
20
22
  def start
21
23
  ScreenRecorder.logger.debug 'Starting recorder...'
22
- @video = nil # New file
23
- @process = start_ffmpeg
24
+ @video = nil # New file
25
+ @process = start_ffmpeg
24
26
  ScreenRecorder.logger.info 'Recording...'
25
27
  @process
26
28
  end
@@ -30,10 +32,12 @@ module ScreenRecorder
30
32
  #
31
33
  def stop
32
34
  ScreenRecorder.logger.debug 'Stopping ffmpeg...'
33
- stop_ffmpeg
35
+ exit_code = stop_ffmpeg
36
+ return if exit_code == 1 # recording failed
37
+
34
38
  ScreenRecorder.logger.debug 'Stopped ffmpeg.'
35
39
  ScreenRecorder.logger.info 'Recording complete.'
36
- @video = FFMPEG::Movie.new(options.output)
40
+ @video = prepare_video unless exit_code == 1
37
41
  end
38
42
 
39
43
  #
@@ -42,7 +46,7 @@ module ScreenRecorder
42
46
  # needed.
43
47
  #
44
48
  def discard
45
- FileUtils.rm options.output
49
+ File.delete options.output
46
50
  end
47
51
 
48
52
  alias delete discard
@@ -54,17 +58,9 @@ module ScreenRecorder
54
58
  # the given options.
55
59
  #
56
60
  def start_ffmpeg
57
- raise Errors::DependencyNotFound, 'ffmpeg binary not found.' unless ffmpeg_exists?
58
-
59
- ScreenRecorder.logger.debug "Command: #{command}"
60
- process = build_command
61
- @log_file = File.new(options.log, 'w+')
62
- process.io.stdout = process.io.stderr = @log_file
63
- @log_file.sync = true
64
- process.duplex = true
65
- process.start
66
- sleep(1.5) # Takes ~1.5s on average to initialize
67
- # Stopped because of an error
61
+ process = execute_command(ffmpeg_command, options.log)
62
+ sleep(1.5) # Takes ~1.5s to initialize ffmpeg
63
+ # Check if it exited unexpectedly
68
64
  raise FFMPEG::Error, "Failed to start ffmpeg. Reason: #{lines_from_log(:last, 2)}" if process.exited?
69
65
 
70
66
  process
@@ -78,37 +74,51 @@ module ScreenRecorder
78
74
  @process.io.stdin.puts 'q' # Gracefully exit ffmpeg
79
75
  @process.io.stdin.close
80
76
  @log_file.close
81
- @process.poll_for_exit(PROCESS_TIMEOUT)
82
- @process.exit_code
83
- rescue ChildProcess::TimeoutError
84
- ScreenRecorder.logger.error 'FFmpeg failed to stop. Force killing it...'
85
- @process.stop # Tries increasingly harsher methods to kill the process.
86
- ScreenRecorder.logger.error "Check '#{@options.log}' for more information."
77
+ wait_for_process_exit(@process)
78
+ end
79
+
80
+ #
81
+ # Runs ffprobe on the output video file and returns
82
+ # a FFMPEG::Movie object.
83
+ #
84
+ def prepare_video
85
+ max_attempts = 3
86
+ attempts_made = 0
87
+ delay = 1.0
88
+
89
+ begin # Fixes #79
90
+ ScreenRecorder.logger.info 'Running ffprobe to prepare video (output) file.'
91
+ FFMPEG::Movie.new(options.output)
92
+ rescue Errno::EAGAIN, Errno::EACCES
93
+ attempts_made += 1
94
+ ScreenRecorder.logger.error "Failed to run ffprobe. Retrying... (#{attempts_made}/#{max_attempts})"
95
+ sleep(delay)
96
+ retry if attempts_made < max_attempts
97
+ raise
98
+ end
99
+ end
100
+
101
+ def ffmpeg_bin
102
+ "#{ScreenRecorder.ffmpeg_binary} -y"
87
103
  end
88
104
 
89
105
  #
90
106
  # Generates the command line arguments based on the given
91
107
  # options.
92
108
  #
93
- def command
94
- cmd = "#{ScreenRecorder.ffmpeg_binary} -y "
95
- cmd << @options.parsed
109
+ def ffmpeg_command
110
+ "#{ffmpeg_bin} #{@options.parsed}"
96
111
  end
97
112
 
98
113
  #
99
114
  # Returns true if ffmpeg binary is found.
100
115
  #
101
116
  def ffmpeg_exists?
102
- return !`which ffmpeg`.empty? if OS.linux? # "" if not found
103
-
104
- return !`where ffmpeg`.empty? if OS.windows?
117
+ return true if FFMPEG.ffmpeg_binary
105
118
 
106
- # If the user does not use ScreenRecorder.ffmpeg_binary=() to set the binary path,
107
- # ScreenRecorder.ffmpeg_binary returns 'ffmpeg' assuming it must be in ENV. However,
108
- # if the above two checks fail, it is not in the ENV either.
109
- return false if ScreenRecorder.ffmpeg_binary == 'ffmpeg'
110
-
111
- true
119
+ false
120
+ rescue Errno::ENOENT # Raised when binary is not set in project or found in ENV
121
+ false
112
122
  end
113
123
 
114
124
  #
@@ -125,15 +135,48 @@ module ScreenRecorder
125
135
  end
126
136
 
127
137
  #
128
- # Returns OS specific arguments for Childprocess.build
138
+ # Executes the given command and outputs to the
139
+ # optional logfile
140
+ #
141
+ def execute_command(cmd, logfile = nil)
142
+ ScreenRecorder.logger.debug "Executing command: #{cmd}"
143
+ process = new_process(cmd)
144
+ process.duplex = true
145
+ if logfile
146
+ @log_file = File.new(logfile, 'w+')
147
+ process.io.stdout = process.io.stderr = @log_file
148
+ @log_file.sync = true
149
+ end
150
+ process.start
151
+ process
152
+ end
153
+
129
154
  #
130
- def build_command
131
- ChildProcess.posix_spawn = true # Support JRuby.
155
+ # Calls Childprocess.new with OS specific arguments
156
+ # to start the given process.
157
+ #
158
+ def new_process(process)
159
+ ChildProcess.posix_spawn = true if RUBY_PLATFORM == 'java' # Support JRuby.
132
160
  if OS.windows?
133
- ChildProcess.build('cmd.exe', '/c', command)
161
+ ChildProcess.new('cmd.exe', '/c', process)
134
162
  else
135
- ChildProcess.build('sh', '-c', command)
163
+ ChildProcess.new('sh', '-c', process)
136
164
  end
137
165
  end
166
+
167
+ #
168
+ # Waits for given process to exit.
169
+ # Forcefully kills the process if it does not exit within 5 seconds.
170
+ # Returns exit code.
171
+ #
172
+ def wait_for_process_exit(process)
173
+ process.poll_for_exit(PROCESS_TIMEOUT)
174
+ process.exit_code # 0
175
+ rescue ChildProcess::TimeoutError
176
+ ScreenRecorder.logger.error 'ffmpeg failed to stop. Force killing it...'
177
+ process.stop # Tries increasingly harsher methods to kill the process.
178
+ ScreenRecorder.logger.error 'Forcefully killed ffmpeg. Recording failed!'
179
+ 1
180
+ end
138
181
  end
139
182
  end
@@ -2,14 +2,16 @@
2
2
  module ScreenRecorder
3
3
  # @since 1.0.0-beta11
4
4
  class Desktop < Common
5
- DEFAULT_INPUT_WIN = 'desktop'.freeze
5
+ include Screenshot
6
+
7
+ DEFAULT_INPUT_WIN = 'desktop'.freeze
6
8
  DEFAULT_INPUT_LINUX = ':0'.freeze
7
- DEFAULT_INPUT_MAC = '1'.freeze
9
+ DEFAULT_INPUT_MAC = '1'.freeze
8
10
 
9
11
  #
10
12
  # Desktop recording mode.
11
13
  #
12
- def initialize(input: input_by_os, output:, advanced: {})
14
+ def initialize(output:, input: input_by_os, advanced: {})
13
15
  super(input: determine_input(input), output: output, advanced: advanced)
14
16
  end
15
17
 
@@ -1,10 +1,18 @@
1
- module ScreenRecorder
2
- # @since 1.0.0-beta5
3
- module Errors
4
- # @since 1.0.0-beta3
5
- class ApplicationNotFound < StandardError; end
6
-
7
- # @since 1.0.0-beta5
8
- class DependencyNotFound < StandardError; end
9
- end
1
+ module ScreenRecorder
2
+ # @since 1.0.0-beta5
3
+ module Errors
4
+ # @since 1.0.0-beta3
5
+ class ApplicationNotFound < StandardError
6
+ def message
7
+ 'expected application was not found by ffmpeg.'
8
+ end
9
+ end
10
+
11
+ # @since 1.0.0-beta5
12
+ class DependencyNotFound < StandardError
13
+ def message
14
+ 'ffmpeg/ffprobe binary path not set or not found in ENV.'
15
+ end
16
+ end
17
+ end
10
18
  end