active_encode 0.5.0 → 0.8.1
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/.circleci/config.yml +80 -0
- data/.rubocop.yml +9 -70
- data/.rubocop_todo.yml +68 -0
- data/Gemfile +5 -4
- data/README.md +69 -0
- data/active_encode.gemspec +12 -3
- data/app/controllers/active_encode/encode_record_controller.rb +1 -0
- data/app/jobs/active_encode/polling_job.rb +1 -1
- data/app/models/active_encode/encode_record.rb +1 -0
- data/db/migrate/20180822021048_create_active_encode_encode_records.rb +1 -0
- data/db/migrate/20190702153755_add_create_options_to_active_encode_encode_records.rb +6 -0
- data/db/migrate/20190712174821_add_progress_to_active_encode_encode_records.rb +6 -0
- data/lib/active_encode.rb +1 -0
- data/lib/active_encode/base.rb +2 -2
- data/lib/active_encode/callbacks.rb +1 -0
- data/lib/active_encode/core.rb +4 -3
- data/lib/active_encode/engine.rb +1 -0
- data/lib/active_encode/engine_adapter.rb +1 -0
- data/lib/active_encode/engine_adapters.rb +4 -1
- data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +31 -29
- data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +138 -87
- data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +5 -4
- data/lib/active_encode/engine_adapters/media_convert_adapter.rb +399 -0
- data/lib/active_encode/engine_adapters/media_convert_output.rb +104 -0
- data/lib/active_encode/engine_adapters/pass_through_adapter.rb +239 -0
- data/lib/active_encode/engine_adapters/test_adapter.rb +5 -4
- data/lib/active_encode/engine_adapters/zencoder_adapter.rb +3 -2
- data/lib/active_encode/errors.rb +6 -0
- data/lib/active_encode/global_id.rb +2 -1
- data/lib/active_encode/input.rb +3 -2
- data/lib/active_encode/output.rb +3 -2
- data/lib/active_encode/persistence.rb +11 -5
- data/lib/active_encode/polling.rb +3 -2
- data/lib/active_encode/spec/shared_specs.rb +2 -0
- data/{spec/shared_specs/engine_adapter_specs.rb → lib/active_encode/spec/shared_specs/engine_adapter.rb} +37 -38
- data/lib/active_encode/status.rb +1 -0
- data/lib/active_encode/technical_metadata.rb +3 -2
- data/lib/active_encode/version.rb +2 -1
- data/lib/file_locator.rb +8 -9
- data/spec/controllers/encode_record_controller_spec.rb +4 -3
- data/spec/fixtures/ffmpeg/cancelled-id/cancelled +0 -0
- data/spec/fixtures/file with space.low.mp4 +0 -0
- data/spec/fixtures/file with space.mp4 +0 -0
- data/spec/fixtures/fireworks.low.mp4 +0 -0
- data/spec/fixtures/media_convert/endpoints.json +1 -0
- data/spec/fixtures/media_convert/job_canceled.json +412 -0
- data/spec/fixtures/media_convert/job_canceling.json +1 -0
- data/spec/fixtures/media_convert/job_completed.json +359 -0
- data/spec/fixtures/media_convert/job_completed_detail.json +1 -0
- data/spec/fixtures/media_convert/job_completed_detail_query.json +1 -0
- data/spec/fixtures/media_convert/job_completed_empty_detail.json +1 -0
- data/spec/fixtures/media_convert/job_created.json +408 -0
- data/spec/fixtures/media_convert/job_failed.json +406 -0
- data/spec/fixtures/media_convert/job_progressing.json +414 -0
- data/spec/fixtures/pass_through/cancelled-id/cancelled +0 -0
- data/spec/fixtures/pass_through/cancelled-id/input_metadata +90 -0
- data/spec/fixtures/pass_through/completed-id/completed +0 -0
- data/spec/fixtures/pass_through/completed-id/input_metadata +102 -0
- data/spec/fixtures/pass_through/completed-id/output_metadata-high +90 -0
- data/spec/fixtures/pass_through/completed-id/output_metadata-low +90 -0
- data/spec/fixtures/pass_through/completed-id/video-high.mp4 +0 -0
- data/spec/fixtures/pass_through/completed-id/video-low.mp4 +0 -0
- data/spec/fixtures/pass_through/failed-id/error.log +1 -0
- data/spec/fixtures/pass_through/failed-id/input_metadata +90 -0
- data/spec/fixtures/pass_through/running-id/input_metadata +90 -0
- data/spec/integration/elastic_transcoder_adapter_spec.rb +30 -30
- data/spec/integration/ffmpeg_adapter_spec.rb +93 -25
- data/spec/integration/matterhorn_adapter_spec.rb +45 -44
- data/spec/integration/media_convert_adapter_spec.rb +152 -0
- data/spec/integration/pass_through_adapter_spec.rb +151 -0
- data/spec/integration/zencoder_adapter_spec.rb +210 -209
- data/spec/rails_helper.rb +1 -0
- data/spec/routing/encode_record_controller_routing_spec.rb +1 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -12
- data/spec/units/callbacks_spec.rb +3 -2
- data/spec/units/core_spec.rb +26 -25
- data/spec/units/engine_adapter_spec.rb +1 -0
- data/spec/units/file_locator_spec.rb +20 -19
- data/spec/units/global_id_spec.rb +12 -11
- data/spec/units/input_spec.rb +8 -5
- data/spec/units/output_spec.rb +8 -5
- data/spec/units/persistence_spec.rb +15 -11
- data/spec/units/polling_job_spec.rb +7 -6
- data/spec/units/polling_spec.rb +1 -0
- data/spec/units/status_spec.rb +3 -3
- metadata +158 -14
- data/.travis.yml +0 -19
data/lib/active_encode/core.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'active_support'
|
2
3
|
require 'active_encode/callbacks'
|
3
4
|
|
@@ -44,19 +45,19 @@ module ActiveEncode
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def initialize(input_url, options = nil)
|
47
|
-
@input = Input.new.tap{ |input| input.url = input_url }
|
48
|
+
@input = Input.new.tap { |input| input.url = input_url }
|
48
49
|
@options = self.class.default_options(input_url).merge(Hash(options))
|
49
50
|
end
|
50
51
|
|
51
52
|
def create!
|
52
53
|
run_callbacks :create do
|
53
|
-
merge!(self.class.engine_adapter.create(
|
54
|
+
merge!(self.class.engine_adapter.create(input.url, options))
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
57
58
|
def cancel!
|
58
59
|
run_callbacks :cancel do
|
59
|
-
merge!(self.class.engine_adapter.cancel(
|
60
|
+
merge!(self.class.engine_adapter.cancel(id))
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
data/lib/active_encode/engine.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module ActiveEncode
|
2
3
|
# == Active Encode adapters
|
3
4
|
#
|
@@ -13,8 +14,10 @@ module ActiveEncode
|
|
13
14
|
autoload :ElasticTranscoderAdapter
|
14
15
|
autoload :TestAdapter
|
15
16
|
autoload :FfmpegAdapter
|
17
|
+
autoload :MediaConvertAdapter
|
18
|
+
autoload :PassThroughAdapter
|
16
19
|
|
17
|
-
ADAPTER = 'Adapter'
|
20
|
+
ADAPTER = 'Adapter'
|
18
21
|
private_constant :ADAPTER
|
19
22
|
|
20
23
|
class << self
|
@@ -1,15 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'addressable/uri'
|
2
|
-
require 'aws-sdk'
|
3
|
+
require 'aws-sdk-elastictranscoder'
|
3
4
|
require 'file_locator'
|
4
5
|
|
5
6
|
module ActiveEncode
|
6
7
|
module EngineAdapters
|
7
8
|
class ElasticTranscoderAdapter
|
8
|
-
|
9
9
|
JOB_STATES = {
|
10
10
|
"Submitted" => :running, "Progressing" => :running, "Canceled" => :cancelled,
|
11
11
|
"Error" => :failed, "Complete" => :completed
|
12
|
-
}
|
12
|
+
}.freeze
|
13
13
|
|
14
14
|
# Require options to include :pipeline_id, :masterfile_bucket and :outputs
|
15
15
|
# Example :outputs value:
|
@@ -29,7 +29,7 @@ module ActiveEncode
|
|
29
29
|
build_encode(job)
|
30
30
|
end
|
31
31
|
|
32
|
-
def find(id,
|
32
|
+
def find(id, _opts = {})
|
33
33
|
build_encode(get_job_details(id))
|
34
34
|
end
|
35
35
|
|
@@ -58,14 +58,14 @@ module ActiveEncode
|
|
58
58
|
return nil if job.nil?
|
59
59
|
encode = ActiveEncode::Base.new(convert_input(job), {})
|
60
60
|
encode.id = job.id
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
61
|
+
encode.state = JOB_STATES[job.status]
|
62
|
+
encode.current_operations = []
|
63
|
+
encode.percent_complete = convert_percent_complete(job)
|
64
|
+
encode.created_at = convert_time(job.timing["submit_time_millis"])
|
65
|
+
encode.updated_at = convert_time(job.timing["finish_time_millis"]) || convert_time(job.timing["start_time_millis"]) || encode.created_at
|
66
66
|
|
67
|
-
|
68
|
-
|
67
|
+
encode.output = convert_output(job)
|
68
|
+
encode.errors = job.outputs.select { |o| o.status == "Error" }.collect(&:status_detail).compact
|
69
69
|
|
70
70
|
tech_md = convert_tech_metadata(job.input.detected_properties)
|
71
71
|
[:width, :height, :frame_rate, :duration, :file_size].each do |field|
|
@@ -82,7 +82,7 @@ module ActiveEncode
|
|
82
82
|
|
83
83
|
def convert_time(time_millis)
|
84
84
|
return nil if time_millis.nil?
|
85
|
-
Time.at(time_millis / 1000)
|
85
|
+
Time.at(time_millis / 1000).utc
|
86
86
|
end
|
87
87
|
|
88
88
|
def convert_bitrate(rate)
|
@@ -121,38 +121,38 @@ module ActiveEncode
|
|
121
121
|
end
|
122
122
|
|
123
123
|
def convert_input(job)
|
124
|
-
job.input
|
124
|
+
job.input.key
|
125
125
|
end
|
126
126
|
|
127
|
-
def copy_to_input_bucket
|
127
|
+
def copy_to_input_bucket(input_url, bucket)
|
128
128
|
case Addressable::URI.parse(input_url).scheme
|
129
|
-
when nil,'file'
|
129
|
+
when nil, 'file'
|
130
130
|
upload_to_s3 input_url, bucket
|
131
131
|
when 's3'
|
132
132
|
check_s3_bucket input_url, bucket
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
136
|
-
def check_s3_bucket
|
136
|
+
def check_s3_bucket(input_url, source_bucket)
|
137
137
|
# logger.info("Checking `#{input_url}'")
|
138
138
|
s3_object = FileLocator::S3File.new(input_url).object
|
139
139
|
if s3_object.bucket_name == source_bucket
|
140
140
|
# logger.info("Already in bucket `#{source_bucket}'")
|
141
141
|
s3_object.key
|
142
142
|
else
|
143
|
-
s3_key = File.join(SecureRandom.uuid,s3_object.key)
|
143
|
+
s3_key = File.join(SecureRandom.uuid, s3_object.key)
|
144
144
|
# logger.info("Copying to `#{source_bucket}/#{input_url}'")
|
145
145
|
target = Aws::S3::Object.new(bucket_name: source_bucket, key: input_url)
|
146
|
-
target.copy_from(s3_object, multipart_copy: s3_object.size >
|
146
|
+
target.copy_from(s3_object, multipart_copy: s3_object.size > 15_728_640) # 15.megabytes
|
147
147
|
s3_key
|
148
148
|
end
|
149
149
|
end
|
150
150
|
|
151
|
-
def upload_to_s3
|
152
|
-
original_input = input_url
|
151
|
+
def upload_to_s3(input_url, source_bucket)
|
152
|
+
# original_input = input_url
|
153
153
|
bucket = Aws::S3::Resource.new(client: s3client).bucket(source_bucket)
|
154
154
|
filename = FileLocator.new(input_url).location
|
155
|
-
s3_key = File.join(SecureRandom.uuid,File.basename(filename))
|
155
|
+
s3_key = File.join(SecureRandom.uuid, File.basename(filename))
|
156
156
|
# logger.info("Copying `#{original_input}' to `#{source_bucket}/#{input_url}'")
|
157
157
|
obj = bucket.object(s3_key)
|
158
158
|
obj.upload_file filename
|
@@ -161,20 +161,22 @@ module ActiveEncode
|
|
161
161
|
end
|
162
162
|
|
163
163
|
def read_preset(id)
|
164
|
-
|
164
|
+
@presets ||= {}
|
165
|
+
@presets[id] ||= client.read_preset(id: id).preset
|
165
166
|
end
|
166
167
|
|
167
168
|
def convert_output(job)
|
168
|
-
pipeline
|
169
|
+
@pipeline ||= client.read_pipeline(id: job.pipeline_id).pipeline
|
169
170
|
job.outputs.collect do |joutput|
|
170
171
|
preset = read_preset(joutput.preset_id)
|
171
172
|
extension = preset.container == 'ts' ? '.m3u8' : ''
|
172
|
-
|
173
|
+
additional_metadata = {
|
173
174
|
managed: false,
|
174
175
|
id: joutput.id,
|
175
176
|
label: joutput.key.split("/", 2).first,
|
176
|
-
url: "s3://#{pipeline.output_bucket}/#{job.output_key_prefix}#{joutput.key}#{extension}"
|
177
|
-
}
|
177
|
+
url: "s3://#{@pipeline.output_bucket}/#{job.output_key_prefix}#{joutput.key}#{extension}"
|
178
|
+
}
|
179
|
+
tech_md = convert_tech_metadata(joutput, preset).merge(additional_metadata)
|
178
180
|
|
179
181
|
output = ActiveEncode::Output.new
|
180
182
|
output.state = convert_state(joutput)
|
@@ -194,7 +196,7 @@ module ActiveEncode
|
|
194
196
|
job.outputs.select { |o| o.status == "Error" }.collect(&:status_detail).compact
|
195
197
|
end
|
196
198
|
|
197
|
-
def convert_tech_metadata(props, preset=nil)
|
199
|
+
def convert_tech_metadata(props, preset = nil)
|
198
200
|
return {} if props.nil? || props.empty?
|
199
201
|
metadata_fields = {
|
200
202
|
file_size: { key: :file_size, method: :itself },
|
@@ -216,13 +218,13 @@ module ActiveEncode
|
|
216
218
|
unless preset.nil?
|
217
219
|
audio = preset.audio
|
218
220
|
video = preset.video
|
219
|
-
metadata.merge!(
|
221
|
+
metadata.merge!(
|
220
222
|
audio_codec: audio&.codec,
|
221
223
|
audio_channels: audio&.channels,
|
222
224
|
audio_bitrate: convert_bitrate(audio&.bit_rate),
|
223
225
|
video_codec: video&.codec,
|
224
226
|
video_bitrate: convert_bitrate(video&.bit_rate)
|
225
|
-
|
227
|
+
)
|
226
228
|
end
|
227
229
|
|
228
230
|
metadata
|
@@ -1,16 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'fileutils'
|
2
3
|
require 'nokogiri'
|
4
|
+
require 'shellwords'
|
3
5
|
|
4
6
|
module ActiveEncode
|
5
7
|
module EngineAdapters
|
6
8
|
class FfmpegAdapter
|
7
9
|
WORK_DIR = ENV["ENCODE_WORK_DIR"] || "encodes" # Should read from config
|
10
|
+
MEDIAINFO_PATH = ENV["MEDIAINFO_PATH"] || "mediainfo"
|
11
|
+
FFMPEG_PATH = ENV["FFMPEG_PATH"] || "ffmpeg"
|
8
12
|
|
9
13
|
def create(input_url, options = {})
|
14
|
+
# Decode file uris for ffmpeg (mediainfo works either way)
|
15
|
+
case input_url
|
16
|
+
when /^file\:\/\/\//
|
17
|
+
input_url = URI.decode(input_url)
|
18
|
+
when /^s3\:\/\//
|
19
|
+
require 'file_locator'
|
20
|
+
|
21
|
+
s3_object = FileLocator::S3File.new(input_url).object
|
22
|
+
input_url = URI.parse(s3_object.presigned_url(:get))
|
23
|
+
end
|
24
|
+
|
10
25
|
new_encode = ActiveEncode::Base.new(input_url, options)
|
11
26
|
new_encode.id = SecureRandom.uuid
|
12
|
-
new_encode.created_at = Time.
|
13
|
-
new_encode.updated_at = Time.
|
27
|
+
new_encode.created_at = Time.now.utc
|
28
|
+
new_encode.updated_at = Time.now.utc
|
14
29
|
new_encode.current_operations = []
|
15
30
|
new_encode.output = []
|
16
31
|
|
@@ -19,18 +34,24 @@ module ActiveEncode
|
|
19
34
|
FileUtils.mkdir_p working_path("outputs", new_encode.id)
|
20
35
|
|
21
36
|
# Extract technical metadata from input file
|
22
|
-
|
37
|
+
curl_option = if options && options[:headers]
|
38
|
+
headers = options[:headers].map { |k, v| "#{k}: #{v}" }
|
39
|
+
(["--File_curl=HttpHeader"] + headers).join(",").yield_self { |s| "'#{s}'" }
|
40
|
+
else
|
41
|
+
""
|
42
|
+
end
|
43
|
+
`#{MEDIAINFO_PATH} #{curl_option} --Output=XML --LogFile=#{working_path("input_metadata", new_encode.id)} "#{input_url}"`
|
23
44
|
new_encode.input = build_input new_encode
|
24
45
|
|
25
46
|
if new_encode.input.duration.blank?
|
26
47
|
new_encode.state = :failed
|
27
48
|
new_encode.percent_complete = 1
|
28
49
|
|
29
|
-
if new_encode.input.file_size.blank?
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
50
|
+
new_encode.errors = if new_encode.input.file_size.blank?
|
51
|
+
["#{input_url} does not exist or is not accessible"]
|
52
|
+
else
|
53
|
+
["Error inspecting input: #{input_url}"]
|
54
|
+
end
|
34
55
|
|
35
56
|
write_errors new_encode
|
36
57
|
return new_encode
|
@@ -42,20 +63,27 @@ module ActiveEncode
|
|
42
63
|
|
43
64
|
# Run the ffmpeg command and save its pid
|
44
65
|
command = ffmpeg_command(input_url, new_encode.id, options)
|
45
|
-
pid = Process.spawn(command)
|
66
|
+
pid = Process.spawn(command, err: working_path('error.log', new_encode.id))
|
46
67
|
File.open(working_path("pid", new_encode.id), 'w') { |file| file.write pid }
|
47
68
|
new_encode.input.id = pid
|
48
69
|
|
49
|
-
# Prevent zombie process
|
50
|
-
Process.detach(pid)
|
51
|
-
|
52
70
|
new_encode
|
71
|
+
rescue StandardError => e
|
72
|
+
new_encode.state = :failed
|
73
|
+
new_encode.percent_complete = 1
|
74
|
+
new_encode.errors = [e.full_message]
|
75
|
+
write_errors new_encode
|
76
|
+
return new_encode
|
77
|
+
ensure
|
78
|
+
# Prevent zombie process
|
79
|
+
Process.detach(pid) if pid.present?
|
53
80
|
end
|
54
81
|
|
55
82
|
# Return encode object from file system
|
56
|
-
def find(id, opts={})
|
83
|
+
def find(id, opts = {})
|
57
84
|
encode_class = opts[:cast]
|
58
|
-
|
85
|
+
encode_class ||= ActiveEncode::Base
|
86
|
+
encode = encode_class.new(nil, opts)
|
59
87
|
encode.id = id
|
60
88
|
encode.output = []
|
61
89
|
encode.created_at, encode.updated_at = get_times encode.id
|
@@ -65,28 +93,21 @@ module ActiveEncode
|
|
65
93
|
pid = get_pid(id)
|
66
94
|
encode.input.id = pid if pid.present?
|
67
95
|
|
68
|
-
if File.file? working_path("error.log", id)
|
69
|
-
error = File.read working_path("error.log", id)
|
70
|
-
if error.present?
|
71
|
-
encode.state = :failed
|
72
|
-
encode.errors = [error]
|
73
|
-
|
74
|
-
return encode
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
encode.errors = []
|
79
|
-
|
80
96
|
encode.current_operations = []
|
81
97
|
encode.created_at, encode.updated_at = get_times encode.id
|
82
|
-
|
83
|
-
if
|
98
|
+
encode.errors = read_errors(id)
|
99
|
+
if encode.errors.present?
|
100
|
+
encode.state = :failed
|
101
|
+
elsif running? pid
|
84
102
|
encode.state = :running
|
85
103
|
encode.current_operations = ["transcoding"]
|
86
104
|
elsif progress_ended?(encode.id) && encode.percent_complete == 100
|
87
|
-
|
105
|
+
encode.state = :completed
|
106
|
+
elsif cancelled? encode.id
|
107
|
+
encode.state = :cancelled
|
88
108
|
elsif encode.percent_complete < 100
|
89
|
-
|
109
|
+
encode.errors << "Encoding has completed but the output duration is shorter than the input"
|
110
|
+
encode.state = :failed
|
90
111
|
end
|
91
112
|
|
92
113
|
encode.output = build_outputs encode if encode.completed?
|
@@ -96,31 +117,61 @@ module ActiveEncode
|
|
96
117
|
|
97
118
|
# Cancel ongoing encode using pid file
|
98
119
|
def cancel(id)
|
99
|
-
|
100
|
-
|
120
|
+
encode = find id
|
121
|
+
if encode.running?
|
122
|
+
pid = get_pid(id)
|
123
|
+
|
124
|
+
IO.popen("ps -ef | grep #{pid}") do |pipe|
|
125
|
+
child_pids = pipe.readlines.map do |line|
|
126
|
+
parts = line.split(/\s+/)
|
127
|
+
parts[1] if parts[2] == pid.to_s && parts[1] != pipe.pid.to_s
|
128
|
+
end.compact
|
129
|
+
|
130
|
+
child_pids.each do |cpid|
|
131
|
+
Process.kill 'SIGTERM', cpid.to_i
|
132
|
+
end
|
133
|
+
end
|
101
134
|
|
102
|
-
|
135
|
+
Process.kill 'SIGTERM', pid.to_i
|
136
|
+
File.write(working_path("cancelled", id), "")
|
137
|
+
encode = find id
|
138
|
+
end
|
139
|
+
encode
|
140
|
+
rescue Errno::ESRCH
|
141
|
+
raise NotRunningError
|
142
|
+
rescue StandardError
|
143
|
+
raise CancelError
|
103
144
|
end
|
104
145
|
|
105
|
-
private
|
146
|
+
private
|
106
147
|
|
107
|
-
def get_times
|
148
|
+
def get_times(id)
|
108
149
|
updated_at = if File.file? working_path("progress", id)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
150
|
+
File.mtime(working_path("progress", id))
|
151
|
+
elsif File.file? working_path("error.log", id)
|
152
|
+
File.mtime(working_path("error.log", id))
|
153
|
+
else
|
154
|
+
File.mtime(working_path("input_metadata", id))
|
155
|
+
end
|
156
|
+
|
157
|
+
[File.mtime(working_path("input_metadata", id)), updated_at]
|
117
158
|
end
|
118
159
|
|
119
|
-
def write_errors
|
160
|
+
def write_errors(encode)
|
120
161
|
File.write(working_path("error.log", encode.id), encode.errors.join("\n"))
|
121
162
|
end
|
122
163
|
|
123
|
-
def
|
164
|
+
def read_errors(id)
|
165
|
+
err_path = working_path("error.log", id)
|
166
|
+
error = File.read(err_path) if File.file? err_path
|
167
|
+
if error.present?
|
168
|
+
[error]
|
169
|
+
else
|
170
|
+
[]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def build_input(encode)
|
124
175
|
input = ActiveEncode::Input.new
|
125
176
|
metadata = get_tech_metadata(working_path("input_metadata", encode.id))
|
126
177
|
input.url = metadata[:url]
|
@@ -132,22 +183,21 @@ private
|
|
132
183
|
input
|
133
184
|
end
|
134
185
|
|
135
|
-
def build_outputs
|
186
|
+
def build_outputs(encode)
|
136
187
|
id = encode.id
|
137
188
|
outputs = []
|
138
189
|
Dir["#{File.absolute_path(working_path('outputs', id))}/*"].each do |file_path|
|
139
190
|
output = ActiveEncode::Output.new
|
140
191
|
output.url = "file://#{file_path}"
|
141
|
-
|
142
|
-
|
143
|
-
output.label = file_path[/#{Regexp.quote(original_filename)}\-(.*?)#{Regexp.quote(File.extname(file_path))}$/, 1]
|
192
|
+
sanitized_filename = sanitize_base encode.input.url
|
193
|
+
output.label = file_path[/#{Regexp.quote(sanitized_filename)}\-(.*?)#{Regexp.quote(File.extname(file_path))}$/, 1]
|
144
194
|
output.id = "#{encode.input.id}-#{output.label}"
|
145
195
|
output.created_at = encode.created_at
|
146
196
|
output.updated_at = File.mtime file_path
|
147
197
|
|
148
198
|
# Extract technical metadata from output file
|
149
199
|
metadata_path = working_path("output_metadata-#{output.label}", id)
|
150
|
-
|
200
|
+
`#{MEDIAINFO_PATH} --Output=XML --LogFile=#{metadata_path} #{output.url}` unless File.file? metadata_path
|
151
201
|
output.assign_tech_metadata(get_tech_metadata(metadata_path))
|
152
202
|
|
153
203
|
outputs << output
|
@@ -158,70 +208,75 @@ private
|
|
158
208
|
|
159
209
|
def ffmpeg_command(input_url, id, opts)
|
160
210
|
output_opt = opts[:outputs].collect do |output|
|
161
|
-
|
211
|
+
sanitized_filename = sanitize_base input_url
|
212
|
+
file_name = "outputs/#{sanitized_filename}-#{output[:label]}.#{output[:extension]}"
|
162
213
|
" #{output[:ffmpeg_opt]} #{working_path(file_name, id)}"
|
163
214
|
end.join(" ")
|
164
|
-
|
165
|
-
|
215
|
+
header_opt = Array(opts[:headers]).map do |k, v|
|
216
|
+
"#{k}: #{v}\r\n"
|
217
|
+
end.join
|
218
|
+
header_opt = "-headers '#{header_opt}'" if header_opt.present?
|
219
|
+
"#{FFMPEG_PATH} #{header_opt} -y -loglevel error -progress #{working_path('progress', id)} -i \"#{input_url}\" #{output_opt}"
|
166
220
|
end
|
167
221
|
|
168
|
-
def
|
169
|
-
if
|
170
|
-
File.
|
222
|
+
def sanitize_base(input_url)
|
223
|
+
if input_url.is_a? URI::HTTP
|
224
|
+
File.basename(input_url.path, File.extname(input_url.path))
|
171
225
|
else
|
172
|
-
|
226
|
+
File.basename(input_url, File.extname(input_url)).gsub(/[^0-9A-Za-z.\-]/, '_')
|
173
227
|
end
|
174
228
|
end
|
175
229
|
|
230
|
+
def get_pid(id)
|
231
|
+
File.read(working_path("pid", id)).remove("\n") if File.file? working_path("pid", id)
|
232
|
+
end
|
233
|
+
|
176
234
|
def working_path(path, id)
|
177
235
|
File.join(WORK_DIR, id, path)
|
178
236
|
end
|
179
237
|
|
180
238
|
def running?(pid)
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
false
|
186
|
-
end
|
239
|
+
Process.getpgid pid.to_i
|
240
|
+
true
|
241
|
+
rescue Errno::ESRCH
|
242
|
+
false
|
187
243
|
end
|
188
244
|
|
189
|
-
def calculate_percent_complete
|
245
|
+
def calculate_percent_complete(encode)
|
190
246
|
data = read_progress encode.id
|
191
247
|
if data.blank?
|
192
248
|
1
|
193
249
|
else
|
194
250
|
progress_in_milliseconds = progress_value("out_time_ms=", data).to_i / 1000.0
|
195
|
-
(progress_in_milliseconds / encode.input.duration * 100).
|
251
|
+
output = (progress_in_milliseconds / encode.input.duration * 100).ceil
|
252
|
+
return 100 if output > 100
|
253
|
+
output
|
196
254
|
end
|
197
255
|
end
|
198
256
|
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
257
|
+
def cancelled?(id)
|
258
|
+
File.exist? working_path("cancelled", id)
|
259
|
+
end
|
260
|
+
|
261
|
+
def read_progress(id)
|
262
|
+
File.read working_path("progress", id) if File.file? working_path("progress", id)
|
205
263
|
end
|
206
264
|
|
207
|
-
def progress_ended?
|
265
|
+
def progress_ended?(id)
|
208
266
|
"end" == progress_value("progress=", read_progress(id))
|
209
267
|
end
|
210
268
|
|
211
|
-
def progress_value
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
else
|
216
|
-
nil
|
217
|
-
end
|
269
|
+
def progress_value(key, data)
|
270
|
+
return nil unless data.present? && key.present?
|
271
|
+
ri = data.rindex(key) + key.length
|
272
|
+
data[ri..data.index("\n", ri) - 1]
|
218
273
|
end
|
219
274
|
|
220
|
-
def get_tech_metadata
|
275
|
+
def get_tech_metadata(file_path)
|
221
276
|
doc = Nokogiri::XML File.read(file_path)
|
222
277
|
doc.remove_namespaces!
|
223
278
|
duration = get_xpath_text(doc, '//Duration/text()', :to_f)
|
224
|
-
duration
|
279
|
+
duration *= 1000 unless duration.nil? # Convert to milliseconds
|
225
280
|
{ url: get_xpath_text(doc, '//media/@ref', :to_s),
|
226
281
|
width: get_xpath_text(doc, '//Width/text()', :to_f),
|
227
282
|
height: get_xpath_text(doc, '//Height/text()', :to_f),
|
@@ -234,12 +289,8 @@ private
|
|
234
289
|
video_bitrate: get_xpath_text(doc, '//track[@type="Video"]/BitRate/text()', :to_i) }
|
235
290
|
end
|
236
291
|
|
237
|
-
def get_xpath_text
|
238
|
-
|
239
|
-
doc.xpath(xpath).first.text.send(cast_method)
|
240
|
-
else
|
241
|
-
nil
|
242
|
-
end
|
292
|
+
def get_xpath_text(doc, xpath, cast_method)
|
293
|
+
doc.xpath(xpath).first&.text&.send(cast_method)
|
243
294
|
end
|
244
295
|
end
|
245
296
|
end
|