screen-recorder 1.1.0 → 1.5.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/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