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.
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