screen-recorder 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
3
- require 'rubocop/rake_task'
4
-
5
- RuboCop::RakeTask.new do |task|
6
- task.requires << 'rubocop-performance'
7
- end
8
-
9
- RSpec::Core::RakeTask.new(:spec)
10
-
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RuboCop::RakeTask.new do |task|
6
+ task.requires << 'rubocop-performance'
7
+ end
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
11
  task default: %w[spec rubocop]
data/bin/console CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
- require 'screen-recorder'
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require 'irb'
14
- IRB.start(__FILE__)
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'screen-recorder'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -1,6 +1,7 @@
1
- require 'streamio-ffmpeg'
2
- require 'os'
1
+ require 'childprocess'
3
2
  require 'logger'
3
+ require 'os'
4
+ require 'streamio-ffmpeg'
4
5
 
5
6
  # @since 1.0.0.beta11
6
7
  module ScreenRecorder
@@ -11,14 +12,41 @@ module ScreenRecorder
11
12
  # ScreenRecorder.ffmpeg_binary = 'C:\ffmpeg.exe'
12
13
  #
13
14
  def self.ffmpeg_binary=(bin)
15
+ ScreenRecorder.logger.debug 'Setting ffmpeg path...'
14
16
  FFMPEG.ffmpeg_binary = bin
17
+ ScreenRecorder.logger.debug "ffmpeg path set: #{bin}"
18
+ ScreenRecorder.ffmpeg_binary
15
19
  end
16
20
 
17
21
  #
18
- # Returns path to ffmpeg binary
22
+ # Returns path to ffmpeg binary or raises DependencyNotFound
19
23
  #
20
24
  def self.ffmpeg_binary
21
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
22
50
  end
23
51
 
24
52
  #
@@ -34,9 +62,9 @@ module ScreenRecorder
34
62
  def self.logger
35
63
  return @logger if @logger
36
64
 
37
- logger = Logger.new(STDOUT)
38
- logger.level = Logger::ERROR
39
- logger.progname = 'ScreenRecorder'
65
+ logger = Logger.new($stdout)
66
+ logger.level = Logger::ERROR
67
+ logger.progname = 'ScreenRecorder'
40
68
  logger.formatter = proc do |severity, time, progname, msg|
41
69
  "#{time.strftime('%F %T')} #{progname} - #{severity} - #{msg}\n"
42
70
  end
@@ -1,10 +1,16 @@
1
1
  # @since 1.0.0-beta11
2
2
  module ScreenRecorder
3
3
  # @since 1.0.0-beta11
4
+ #
5
+ # @api private
4
6
  class Common
7
+ PROCESS_TIMEOUT = 5 # Seconds to wait for ffmpeg to quit
8
+
5
9
  attr_reader :options, :video
6
10
 
7
11
  def initialize(input:, output:, advanced: {})
12
+ raise Errors::DependencyNotFound unless ffmpeg_exists?
13
+
8
14
  @options = Options.new(input: input, output: output, advanced: advanced)
9
15
  @video = nil
10
16
  @process = nil
@@ -14,11 +20,9 @@ module ScreenRecorder
14
20
  # Starts the recording
15
21
  #
16
22
  def start
17
- @video = nil # New file
18
- start_time = Time.now
19
- @process = start_ffmpeg
20
- elapsed = Time.now - start_time
21
- ScreenRecorder.logger.debug "Process started in #{elapsed}s"
23
+ ScreenRecorder.logger.debug 'Starting recorder...'
24
+ @video = nil # New file
25
+ @process = start_ffmpeg
22
26
  ScreenRecorder.logger.info 'Recording...'
23
27
  @process
24
28
  end
@@ -27,11 +31,27 @@ module ScreenRecorder
27
31
  # Stops the recording
28
32
  #
29
33
  def stop
30
- ScreenRecorder.logger.debug 'Stopping ffmpeg.exe...'
31
- elapsed = stop_ffmpeg
32
- ScreenRecorder.logger.debug "Stopped ffmpeg.exe in #{elapsed}s"
34
+ ScreenRecorder.logger.debug 'Stopping ffmpeg...'
35
+ exit_code = stop_ffmpeg
36
+ return if exit_code == 1 # recording failed
37
+
38
+ ScreenRecorder.logger.debug 'Stopped ffmpeg.'
33
39
  ScreenRecorder.logger.info 'Recording complete.'
34
- @video = FFMPEG::Movie.new(options.output)
40
+ @video = prepare_video unless exit_code == 1
41
+ end
42
+
43
+ #
44
+ # Takes a screenshot in the current context (input) - desktop or current window
45
+ #
46
+ def screenshot(filename)
47
+ process = execute_command(screenshot_cmd(filename))
48
+ exit_code = wait_for_process_exit(process) # 0 (success) or 1 (fail)
49
+ if exit_code&.zero?
50
+ ScreenRecorder.logger.info "Screenshot: #{filename}"
51
+ return filename
52
+ end
53
+ ScreenRecorder.logger.error 'Failed to take a screenshot.'
54
+ nil
35
55
  end
36
56
 
37
57
  #
@@ -40,7 +60,7 @@ module ScreenRecorder
40
60
  # needed.
41
61
  #
42
62
  def discard
43
- FileUtils.rm options.output
63
+ File.delete options.output
44
64
  end
45
65
 
46
66
  alias delete discard
@@ -52,75 +72,81 @@ module ScreenRecorder
52
72
  # the given options.
53
73
  #
54
74
  def start_ffmpeg
55
- raise Errors::DependencyNotFound, 'ffmpeg binary not found.' unless ffmpeg_exists?
75
+ process = execute_command(ffmpeg_command, options.log)
76
+ sleep(1.5) # Takes ~1.5s to initialize ffmpeg
77
+ # Check if it exited unexpectedly
78
+ raise FFMPEG::Error, "Failed to start ffmpeg. Reason: #{lines_from_log(:last, 2)}" if process.exited?
56
79
 
57
- ScreenRecorder.logger.debug "Command: #{command}"
58
- process = IO.popen(command, 'r+')
59
- sleep(1.5) # Takes ~1.5s on average to initialize
60
80
  process
61
81
  end
62
82
 
63
83
  #
64
84
  # Sends 'q' to the ffmpeg binary to gracefully stop the process.
65
- # Forcefully terminates it if it takes more than 10s.
85
+ # Forcefully terminates it if it takes more than 5s.
66
86
  #
67
87
  def stop_ffmpeg
68
- @process.puts 'q' # Gracefully exit ffmpeg
69
- elapsed = wait_for_io_eof(10)
70
- @process.close_write # Close IO
71
- elapsed
72
- rescue Timeout::Error
73
- ScreenRecorder.logger.error 'FFmpeg failed to stop. Force killing it...'
74
- force_kill_ffmpeg
75
- ScreenRecorder.logger.error "Check '#{@options.log}' for more information."
76
- rescue Errno::EPIPE
77
- # Gets last line from log file
78
- err_line = get_lines_from_log(:last, 2)
79
- raise FFMPEG::Error, err_line
88
+ @process.io.stdin.puts 'q' # Gracefully exit ffmpeg
89
+ @process.io.stdin.close
90
+ @log_file.close
91
+ wait_for_process_exit(@process)
92
+ end
93
+
94
+ #
95
+ # Runs ffprobe on the output video file and returns
96
+ # a FFMPEG::Movie object.
97
+ #
98
+ def prepare_video
99
+ max_attempts = 3
100
+ attempts_made = 0
101
+ delay = 1.0
102
+
103
+ begin # Fixes #79
104
+ ScreenRecorder.logger.info 'Running ffprobe to prepare video (output) file.'
105
+ FFMPEG::Movie.new(options.output)
106
+ rescue Errno::EAGAIN, Errno::EACCES
107
+ attempts_made += 1
108
+ ScreenRecorder.logger.error "Failed to run ffprobe. Retrying... (#{attempts_made}/#{max_attempts})"
109
+ sleep(delay)
110
+ retry if attempts_made < max_attempts
111
+ raise
112
+ end
113
+ end
114
+
115
+ def ffmpeg_bin
116
+ "#{ScreenRecorder.ffmpeg_binary} -y"
80
117
  end
81
118
 
82
119
  #
83
120
  # Generates the command line arguments based on the given
84
121
  # options.
85
122
  #
86
- def command
87
- cmd = "#{ScreenRecorder.ffmpeg_binary} -y "
88
- cmd << @options.parsed
123
+ def ffmpeg_command
124
+ "#{ffmpeg_bin} #{@options.parsed}"
89
125
  end
90
126
 
91
127
  #
92
- # Waits for IO#eof? to return true
93
- # after 'q' is sent to the ffmpeg process.
128
+ # Parameters to capture a single frame
94
129
  #
95
- def wait_for_io_eof(timeout)
96
- start = Time.now
97
- Timeout.timeout(timeout) do
98
- sleep(0.1) until @process.eof?
99
- end
100
- ScreenRecorder.logger.debug "IO#eof? #{@process.eof?}"
101
- Time.now - start
130
+ def screenshot_cmd(filename)
131
+ # -f overwrites existing file
132
+ "#{ffmpeg_bin} -f #{options.capture_device} -i #{options.input} -framerate 1 -frames:v 1 #{filename}"
102
133
  end
103
134
 
104
135
  #
105
136
  # Returns true if ffmpeg binary is found.
106
137
  #
107
138
  def ffmpeg_exists?
108
- return !`which ffmpeg`.empty? if OS.linux? # "" if not found
109
-
110
- return !`where ffmpeg`.empty? if OS.windows?
139
+ return true if FFMPEG.ffmpeg_binary
111
140
 
112
- # If the user does not use ScreenRecorder.ffmpeg_binary=() to set the binary path,
113
- # ScreenRecorder.ffmpeg_binary returns 'ffmpeg' assuming it must be in ENV. However,
114
- # if the above two checks fail, it is not in the ENV either.
115
- return false if ScreenRecorder.ffmpeg_binary == 'ffmpeg'
116
-
117
- true
141
+ false
142
+ rescue Errno::ENOENT # Raised when binary is not set in project or found in ENV
143
+ false
118
144
  end
119
145
 
120
146
  #
121
147
  # Returns lines from the log file
122
148
  #
123
- def get_lines_from_log(position = :last, count = 2)
149
+ def lines_from_log(position = :last, count = 2)
124
150
  f = File.open(options.log)
125
151
  lines = f.readlines
126
152
  lines = lines.last(count) if position == :last
@@ -131,17 +157,48 @@ module ScreenRecorder
131
157
  end
132
158
 
133
159
  #
134
- # Force kills the ffmpeg process.
160
+ # Executes the given command and outputs to the
161
+ # optional logfile
135
162
  #
136
- def force_kill_ffmpeg
163
+ def execute_command(cmd, logfile = nil)
164
+ ScreenRecorder.logger.debug "Executing command: #{cmd}"
165
+ process = new_process(cmd)
166
+ process.duplex = true
167
+ if logfile
168
+ @log_file = File.new(logfile, 'w+')
169
+ process.io.stdout = process.io.stderr = @log_file
170
+ @log_file.sync = true
171
+ end
172
+ process.start
173
+ process
174
+ end
175
+
176
+ #
177
+ # Calls Childprocess.new with OS specific arguments
178
+ # to start the given process.
179
+ #
180
+ def new_process(process)
181
+ ChildProcess.posix_spawn = true if RUBY_PLATFORM == 'java' # Support JRuby.
137
182
  if OS.windows?
138
- pid = `powershell (Get-Process -name 'ffmpeg').Id`.strip.to_i
139
- `taskkill /f /pid #{pid}`
140
- return
183
+ ChildProcess.new('cmd.exe', '/c', process)
184
+ else
185
+ ChildProcess.new('sh', '-c', process)
141
186
  end
187
+ end
142
188
 
143
- `killall -9 ffmpeg` # Linux and macOS
144
- nil
189
+ #
190
+ # Waits for given process to exit.
191
+ # Forcefully kills the process if it does not exit within 5 seconds.
192
+ # Returns exit code.
193
+ #
194
+ def wait_for_process_exit(process)
195
+ process.poll_for_exit(PROCESS_TIMEOUT)
196
+ process.exit_code # 0
197
+ rescue ChildProcess::TimeoutError
198
+ ScreenRecorder.logger.error 'ffmpeg failed to stop. Force killing it...'
199
+ process.stop # Tries increasingly harsher methods to kill the process.
200
+ ScreenRecorder.logger.error 'Forcefully killed ffmpeg. Recording failed!'
201
+ 1
145
202
  end
146
203
  end
147
204
  end
@@ -2,14 +2,14 @@
2
2
  module ScreenRecorder
3
3
  # @since 1.0.0-beta11
4
4
  class Desktop < Common
5
- DEFAULT_INPUT_WIN = 'desktop'.freeze
5
+ DEFAULT_INPUT_WIN = 'desktop'.freeze
6
6
  DEFAULT_INPUT_LINUX = ':0'.freeze
7
- DEFAULT_INPUT_MAC = '1'.freeze
7
+ DEFAULT_INPUT_MAC = '1'.freeze
8
8
 
9
9
  #
10
- # Desktop recording specific initializer.
10
+ # Desktop recording mode.
11
11
  #
12
- def initialize(input: input_by_os, output:, advanced: {})
12
+ def initialize(output:, input: input_by_os, advanced: {})
13
13
  super(input: determine_input(input), output: output, advanced: advanced)
14
14
  end
15
15
 
@@ -25,7 +25,7 @@ module ScreenRecorder
25
25
 
26
26
  return DEFAULT_INPUT_MAC if OS.mac?
27
27
 
28
- raise NotImplementedError, 'Your OS is not supported. Feel free to create an Issue on GitHub.'
28
+ raise 'Your OS is not supported. Feel free to create an Issue on GitHub.'
29
29
  end
30
30
 
31
31
  #
@@ -2,9 +2,17 @@ module ScreenRecorder
2
2
  # @since 1.0.0-beta5
3
3
  module Errors
4
4
  # @since 1.0.0-beta3
5
- class ApplicationNotFound < StandardError; end
5
+ class ApplicationNotFound < StandardError
6
+ def message
7
+ 'expected application was not found by ffmpeg.'
8
+ end
9
+ end
6
10
 
7
11
  # @since 1.0.0-beta5
8
- class DependencyNotFound < StandardError; end
12
+ class DependencyNotFound < StandardError
13
+ def message
14
+ 'ffmpeg/ffprobe binary path not set or not found in ENV.'
15
+ end
16
+ end
9
17
  end
10
18
  end
@@ -1,26 +1,34 @@
1
1
  # @since 1.0.0-beta11
2
+ #
3
+ # @api private
2
4
  module ScreenRecorder
3
5
  # @since 1.0.0-beta11
4
6
  class Options
5
- DEFAULT_LOG_FILE = 'ffmpeg.log'.freeze
6
- DEFAULT_FPS = 15.0
7
- DEFAULT_INPUT_PIX_FMT = 'uyvy422'.freeze # For macOS / avfoundation
8
- DEFAULT_OUTPUT_PIX_FMT = 'yuv420p'.freeze
7
+ attr_reader :all
8
+
9
+ DEFAULT_LOG_FILE = 'ffmpeg.log'.freeze
10
+ DEFAULT_FPS = 15.0
11
+ DEFAULT_MAC_INPUT_PIX_FMT = 'uyvy422'.freeze # For avfoundation
12
+ DEFAULT_PIX_FMT = 'yuv420p'.freeze
13
+ YUV420P_SCALING = '"scale=trunc(iw/2)*2:trunc(ih/2)*2"'.freeze
9
14
 
10
15
  def initialize(options)
11
- TypeChecker.check options, Hash
12
- TypeChecker.check options[:advanced], Hash if options[:advanced]
13
- @options = verify_options options
14
- advanced[:framerate] ||= DEFAULT_FPS
15
- advanced[:log] ||= DEFAULT_LOG_FILE
16
- advanced[:pix_fmt] ||= DEFAULT_OUTPUT_PIX_FMT
16
+ # @todo Consider using OpenStruct
17
+ @all = verify_options options
18
+ advanced[:input] = default_advanced_input.merge(advanced_input)
19
+ advanced[:output] = default_advanced_output.merge(advanced_output)
20
+ advanced[:log] ||= DEFAULT_LOG_FILE
21
+
22
+ # Fix for using yuv420p pixel format for output
23
+ # @see https://www.reck.dk/ffmpeg-libx264-height-not-divisible-by-2/
24
+ advanced_output[:vf] = YUV420P_SCALING if advanced_output[:pix_fmt] == 'yuv420p'
17
25
  end
18
26
 
19
27
  #
20
28
  # Returns given input file or input
21
29
  #
22
30
  def input
23
- @options[:input]
31
+ @all[:input]
24
32
  end
25
33
 
26
34
  #
@@ -34,21 +42,22 @@ module ScreenRecorder
34
42
  # Returns given output filepath
35
43
  #
36
44
  def output
37
- @options[:output]
45
+ @all[:output]
38
46
  end
39
47
 
40
48
  #
41
49
  # Returns given values that are optional
42
50
  #
43
51
  def advanced
44
- @options[:advanced] ||= {}
52
+ @all[:advanced] ||= {}
45
53
  end
46
54
 
47
55
  #
48
56
  # Returns given framerate
49
57
  #
50
58
  def framerate
51
- advanced[:framerate]
59
+ ScreenRecorder.logger.warn '#framerate will not be available in the next release. Use #advanced instead.'
60
+ advanced[:output][:framerate]
52
61
  end
53
62
 
54
63
  #
@@ -58,28 +67,17 @@ module ScreenRecorder
58
67
  advanced[:log]
59
68
  end
60
69
 
61
- #
62
- # Returns all given options
63
- #
64
- def all
65
- @options
66
- end
67
-
68
70
  #
69
71
  # Returns a String with all options parsed and
70
72
  # ready for the ffmpeg process to use
71
73
  #
72
74
  def parsed
73
75
  vals = "-f #{capture_device} "
74
- vals << "-pix_fmt #{DEFAULT_INPUT_PIX_FMT} " if OS.mac? # Input pixel format
75
- vals << advanced_options unless advanced.empty?
76
- vals << "-i #{input} "
77
- vals << "-pix_fmt #{advanced[:pix_fmt]} " if advanced[:pix_fmt] # Output pixel format
78
- # Fix for using yuv420p
79
- # @see https://www.reck.dk/ffmpeg-libx264-height-not-divisible-by-2/
80
- vals << '-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ' if advanced[:pix_fmt] == 'yuv420p'
76
+ vals << parse_advanced(advanced_input)
77
+ vals << "-i #{input} " unless advanced_input[:i] # Input provided by user
78
+ vals << parse_advanced(advanced)
79
+ vals << parse_advanced(advanced_output)
81
80
  vals << output
82
- vals << ffmpeg_log_to(log)
83
81
  end
84
82
 
85
83
  private
@@ -90,13 +88,36 @@ module ScreenRecorder
90
88
  # options are not present in the given Hash.
91
89
  #
92
90
  def verify_options(options)
91
+ TypeChecker.check options, Hash
92
+ TypeChecker.check options[:advanced], Hash if options[:advanced]
93
93
  missing_options = required_options.select { |req| options[req].nil? }
94
- err = "Required options are missing: #{missing_options}"
94
+ err = "Required options are missing: #{missing_options}"
95
95
  raise(ArgumentError, err) unless missing_options.empty?
96
96
 
97
97
  options
98
98
  end
99
99
 
100
+ def advanced_input
101
+ advanced[:input] ||= {}
102
+ end
103
+
104
+ def advanced_output
105
+ advanced[:output] ||= {}
106
+ end
107
+
108
+ def default_advanced_input
109
+ {
110
+ pix_fmt: OS.mac? ? DEFAULT_MAC_INPUT_PIX_FMT : nil
111
+ }
112
+ end
113
+
114
+ def default_advanced_output
115
+ {
116
+ pix_fmt: DEFAULT_PIX_FMT,
117
+ framerate: advanced[:framerate] || DEFAULT_FPS
118
+ }
119
+ end
120
+
100
121
  #
101
122
  # Returns Array of required options as Symbols
102
123
  #
@@ -105,47 +126,44 @@ module ScreenRecorder
105
126
  end
106
127
 
107
128
  #
108
- # Returns advanced options parsed and ready for ffmpeg to receive.
129
+ # Returns given Hash parsed and ready for ffmpeg to receive.
109
130
  #
110
- def advanced_options
131
+ def parse_advanced(opts)
132
+ # @todo Replace arr with opts.each_with_object([])
111
133
  arr = []
112
-
113
- # Log file and output pixel format is handled separately
114
- # at the end of the command
115
- advanced.reject { |k, _| %i[log pix_fmt].include? k }
134
+ rejects = %i[input output log]
135
+ # Do not parse input/output and log as they're placed separately in #parsed
136
+ opts.reject { |k, _| rejects.include? k }
116
137
  .each do |k, v|
117
- arr.push "-#{k} #{v}"
138
+ arr.push "-#{k} #{v}" unless v.nil? # Ignore blank params
118
139
  end
119
- arr.join(' ') + ' '
140
+ "#{arr.join(' ')} "
120
141
  end
121
142
 
122
143
  #
123
- # Returns logging command with user given log file
124
- # from options or the default file.
144
+ # Returns input capture device based on user given value or the current OS.
125
145
  #
126
- def ffmpeg_log_to(file)
127
- file ||= DEFAULT_LOG_FILE
128
- " 2> #{file}"
146
+ def determine_capture_device
147
+ # User given capture device or format from advanced configs Hash
148
+ # @see https://www.ffmpeg.org/ffmpeg.html#Main-options
149
+ return advanced_input[:f] if advanced_input[:f]
150
+
151
+ return advanced_input[:fmt] if advanced_input[:fmt]
152
+
153
+ default_capture_device
129
154
  end
130
155
 
131
156
  #
132
- # Returns input capture device based on user given value or the current OS.
157
+ # Returns input capture device for current OS.
133
158
  #
134
- def determine_capture_device
135
- # User given capture device or format
136
- # @see https://www.ffmpeg.org/ffmpeg.html#Main-options
137
- return advanced[:f] if advanced[:f]
138
- return advanced[:fmt] if advanced[:fmt]
139
-
140
- if OS.windows?
141
- 'gdigrab'
142
- elsif OS.linux?
143
- 'x11grab'
144
- elsif OS.mac?
145
- 'avfoundation'
146
- else
147
- raise NotImplementedError, 'Your OS is not supported.'
148
- end
159
+ def default_capture_device
160
+ return 'gdigrab' if OS.windows?
161
+
162
+ return 'x11grab' if OS.linux?
163
+
164
+ return 'avfoundation' if OS.mac?
165
+
166
+ raise 'Your OS is not supported. Feel free to create an Issue on GitHub.'
149
167
  end
150
168
  end
151
169
  end