screen-recorder 1.3.0 → 1.6.0
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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +1 -0
- data/.github/workflows/tests.yml +97 -0
- data/.gitignore +99 -99
- data/.rspec +3 -3
- data/.rubocop.yml +41 -7
- data/CHANGELOG.md +20 -1
- data/LICENSE.txt +21 -21
- data/README.md +79 -18
- data/bin/console +0 -0
- data/bin/setup +8 -8
- data/lib/screen-recorder.rb +32 -4
- data/lib/screen-recorder/common.rb +82 -39
- data/lib/screen-recorder/desktop.rb +5 -3
- data/lib/screen-recorder/errors.rb +17 -9
- data/lib/screen-recorder/options.rb +18 -17
- data/lib/screen-recorder/screenshot.rb +39 -0
- data/lib/screen-recorder/version.rb +1 -1
- data/lib/screen-recorder/window.rb +5 -1
- data/screen-recorder.gemspec +13 -13
- metadata +28 -26
- data/.travis.yml +0 -42
- data/appveyor.yml +0 -39
- data/support/install_jruby.ps1 +0 -7
- data/support/start_test_reporter.sh +0 -7
- data/support/start_xvfb.sh +0 -5
- data/support/stop_test_reporter.sh +0 -5
data/README.md
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
# ScreenRecorder
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/screen-recorder)
|
4
|
-
[](https://www.rubydoc.info/
|
5
|
-
[](https://ci.appveyor.com/project/kapoorlakshya/screen-recorder/branch/master)
|
7
|
-
[](https://codeclimate.com/github/kapoorlakshya/screen-recorder/maintainability)
|
8
|
-
[](https://codeclimate.com/github/kapoorlakshya/screen-recorder/test_coverage)
|
4
|
+
[](https://www.rubydoc.info/gems/screen-recorder/)
|
5
|
+
[](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://
|
23
|
+
Linux and macOS instructions are [here](https://trac.ffmpeg.org/wiki/CompilationGuide).
|
27
24
|
|
28
|
-
|
29
|
-
|
30
|
-
|
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/
|
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-
|
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
|
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::
|
107
|
+
ScreenRecorder::('firefox') # Name of exe
|
109
108
|
#=> ["Mozilla Firefox"]
|
110
109
|
|
111
|
-
ScreenRecorder::
|
110
|
+
ScreenRecorder::('chrome')
|
112
111
|
#=> ["New Tab - Google Chrome"]
|
113
112
|
```
|
114
113
|
|
115
|
-
####
|
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
|
data/lib/screen-recorder.rb
CHANGED
@@ -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
|
39
|
-
logger.level
|
40
|
-
logger.progname
|
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
|
23
|
-
@process
|
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 =
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
94
|
-
|
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
|
103
|
-
|
104
|
-
return !`where ffmpeg`.empty? if OS.windows?
|
117
|
+
return true if FFMPEG.ffmpeg_binary
|
105
118
|
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
#
|
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
|
-
|
131
|
-
|
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.
|
161
|
+
ChildProcess.new('cmd.exe', '/c', process)
|
134
162
|
else
|
135
|
-
ChildProcess.
|
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
|
-
|
5
|
+
include Screenshot
|
6
|
+
|
7
|
+
DEFAULT_INPUT_WIN = 'desktop'.freeze
|
6
8
|
DEFAULT_INPUT_LINUX = ':0'.freeze
|
7
|
-
DEFAULT_INPUT_MAC
|
9
|
+
DEFAULT_INPUT_MAC = '1'.freeze
|
8
10
|
|
9
11
|
#
|
10
12
|
# Desktop recording mode.
|
11
13
|
#
|
12
|
-
def initialize(input: input_by_os,
|
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
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|