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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +2 -0
- data/.github/workflows/tests.yml +97 -0
- data/.rspec +1 -1
- data/.rubocop.yml +83 -32
- data/{CHANGES.md → CHANGELOG.md} +37 -1
- data/Gemfile +3 -3
- data/README.md +281 -219
- data/Rakefile +10 -10
- data/bin/console +14 -14
- data/lib/screen-recorder.rb +34 -6
- data/lib/screen-recorder/common.rb +113 -56
- data/lib/screen-recorder/desktop.rb +5 -5
- data/lib/screen-recorder/errors.rb +10 -2
- data/lib/screen-recorder/options.rb +77 -59
- data/lib/screen-recorder/titles.rb +8 -41
- data/lib/screen-recorder/type_checker.rb +2 -0
- data/lib/screen-recorder/version.rb +1 -1
- data/lib/screen-recorder/window.rb +45 -9
- data/screen-recorder.gemspec +16 -12
- metadata +57 -23
- data/.travis.yml +0 -35
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__)
|
data/lib/screen-recorder.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
require '
|
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
|
38
|
-
logger.level
|
39
|
-
logger.progname
|
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
|
-
|
18
|
-
|
19
|
-
@process
|
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
|
31
|
-
|
32
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
70
|
-
@
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
87
|
-
|
88
|
-
cmd << @options.parsed
|
123
|
+
def ffmpeg_command
|
124
|
+
"#{ffmpeg_bin} #{@options.parsed}"
|
89
125
|
end
|
90
126
|
|
91
127
|
#
|
92
|
-
#
|
93
|
-
# after 'q' is sent to the ffmpeg process.
|
128
|
+
# Parameters to capture a single frame
|
94
129
|
#
|
95
|
-
def
|
96
|
-
|
97
|
-
|
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
|
109
|
-
|
110
|
-
return !`where ffmpeg`.empty? if OS.windows?
|
139
|
+
return true if FFMPEG.ffmpeg_binary
|
111
140
|
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
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
|
-
#
|
160
|
+
# Executes the given command and outputs to the
|
161
|
+
# optional logfile
|
135
162
|
#
|
136
|
-
def
|
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
|
-
|
139
|
-
|
140
|
-
|
183
|
+
ChildProcess.new('cmd.exe', '/c', process)
|
184
|
+
else
|
185
|
+
ChildProcess.new('sh', '-c', process)
|
141
186
|
end
|
187
|
+
end
|
142
188
|
|
143
|
-
|
144
|
-
|
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
|
5
|
+
DEFAULT_INPUT_WIN = 'desktop'.freeze
|
6
6
|
DEFAULT_INPUT_LINUX = ':0'.freeze
|
7
|
-
DEFAULT_INPUT_MAC
|
7
|
+
DEFAULT_INPUT_MAC = '1'.freeze
|
8
8
|
|
9
9
|
#
|
10
|
-
# Desktop recording
|
10
|
+
# Desktop recording mode.
|
11
11
|
#
|
12
|
-
def initialize(input: input_by_os,
|
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
|
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
|
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
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
advanced[:
|
15
|
-
advanced[:log]
|
16
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
52
|
+
@all[:advanced] ||= {}
|
45
53
|
end
|
46
54
|
|
47
55
|
#
|
48
56
|
# Returns given framerate
|
49
57
|
#
|
50
58
|
def framerate
|
51
|
-
advanced
|
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 <<
|
75
|
-
vals <<
|
76
|
-
vals <<
|
77
|
-
vals <<
|
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
|
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
|
129
|
+
# Returns given Hash parsed and ready for ffmpeg to receive.
|
109
130
|
#
|
110
|
-
def
|
131
|
+
def parse_advanced(opts)
|
132
|
+
# @todo Replace arr with opts.each_with_object([])
|
111
133
|
arr = []
|
112
|
-
|
113
|
-
#
|
114
|
-
|
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
|
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
|
127
|
-
|
128
|
-
|
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
|
157
|
+
# Returns input capture device for current OS.
|
133
158
|
#
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
return
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|