active_encode 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +26 -18
- data/.rubocop_todo.yml +3 -0
- data/active_encode.gemspec +3 -0
- data/lib/active_encode/base.rb +1 -2
- data/lib/active_encode/engine_adapters.rb +1 -0
- data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +5 -4
- data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +86 -30
- data/lib/active_encode/engine_adapters/pass_through_adapter.rb +239 -0
- data/lib/active_encode/errors.rb +6 -0
- data/lib/active_encode/persistence.rb +5 -3
- data/lib/active_encode/version.rb +1 -1
- data/spec/controllers/encode_record_controller_spec.rb +2 -2
- 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/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/ffmpeg_adapter_spec.rb +66 -2
- data/spec/integration/pass_through_adapter_spec.rb +151 -0
- data/spec/shared_specs/engine_adapter_specs.rb +7 -8
- metadata +50 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5aed6c18b1e8d3b2a0ecb4e8dd296660f2af65ca00973edad791b0315964da4c
|
4
|
+
data.tar.gz: 2ce90e1367403bc7d45ffb3d3ba5ada6991ca1f80d0a1b59944abeb63d47d372
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1edfa784257928dc9d06463434a4132a7ab07995a5679aca9051aa68335dd1ed644cd2c5f9544dc439911f29087d2af1b358c839ef2692a29a0478fae5e5ab2
|
7
|
+
data.tar.gz: f3702f56f989045165a70b29850fc7e480f02c85877912db3115b4a9909e39673090d79fe23127a0a1a30d5b0be96c15f6bba4ea04af751b388531b864786e33
|
data/.circleci/config.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
version: 2.1
|
2
2
|
orbs:
|
3
|
-
samvera: samvera/circleci-orb@0
|
3
|
+
samvera: samvera/circleci-orb@0.3.2
|
4
4
|
jobs:
|
5
5
|
bundle_and_test:
|
6
6
|
parameters:
|
@@ -12,35 +12,50 @@ jobs:
|
|
12
12
|
default: 5.2.3
|
13
13
|
bundler_version:
|
14
14
|
type: string
|
15
|
-
default: 2.0.
|
15
|
+
default: 2.0.2
|
16
|
+
ffmpeg_version:
|
17
|
+
type: string
|
18
|
+
default: 4.1.4
|
16
19
|
executor:
|
17
20
|
name: 'samvera/ruby'
|
18
21
|
ruby_version: << parameters.ruby_version >>
|
19
22
|
environment:
|
20
23
|
ENGINE_CART_RAILS_OPTIONS: --skip-git --skip-bundle --skip-listen --skip-spring --skip-yarn --skip-keeps --skip-coffee --skip-puma --skip-test
|
21
24
|
RAILS_VERSION: << parameters.rails_version >>
|
25
|
+
FFMPEG_PATH: /tmp/ffmpeg
|
22
26
|
working_directory: ~/project
|
23
27
|
steps:
|
24
|
-
- run:
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
- run:
|
29
|
+
command: |
|
30
|
+
sudo apt-get install libmms0
|
31
|
+
sudo wget -P /tmp/ https://mediaarea.net/download/binary/libzen0/0.4.37/libzen0v5_0.4.37-1_amd64.Debian_9.0.deb
|
32
|
+
sudo wget -P /tmp/ https://mediaarea.net/download/binary/libmediainfo0/19.04/libmediainfo0v5_19.04-1_amd64.Debian_9.0.deb
|
33
|
+
sudo wget -P /tmp/ https://mediaarea.net/download/binary/mediainfo/19.04/mediainfo_19.04-1_amd64.Debian_9.0.deb
|
34
|
+
sudo dpkg -i /tmp/libzen0v5_0.4.37-1_amd64.Debian_9.0.deb /tmp/libmediainfo0v5_19.04-1_amd64.Debian_9.0.deb /tmp/mediainfo_19.04-1_amd64.Debian_9.0.deb
|
35
|
+
|
36
|
+
- restore_cache:
|
37
|
+
keys:
|
38
|
+
- v1-ffmpeg-<< parameters.ffmpeg_version >>
|
39
|
+
|
40
|
+
- run: curl https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-<< parameters.ffmpeg_version >>-amd64-static.tar.xz | tar xJ -C /tmp/ --strip-components=1
|
41
|
+
|
42
|
+
- save_cache:
|
43
|
+
key: v1-ffmpeg-<< parameters.ffmpeg_version >>`
|
44
|
+
paths:
|
45
|
+
- /tmp/ffmpeg
|
29
46
|
|
30
47
|
- samvera/cached_checkout
|
31
48
|
|
32
|
-
- samvera/
|
49
|
+
- samvera/bundle:
|
33
50
|
ruby_version: << parameters.ruby_version >>
|
34
51
|
bundler_version: << parameters.bundler_version >>
|
35
|
-
project: active_encode
|
36
52
|
|
37
53
|
- samvera/engine_cart_generate:
|
38
54
|
cache_key: v2-internal-test-app-{{ checksum "active_encode.gemspec" }}-{{ checksum "spec/test_app_templates/lib/generators/test_app_generator.rb" }}-<< parameters.rails_version >>-<< parameters.ruby_version >>
|
39
55
|
|
40
|
-
- samvera/
|
56
|
+
- samvera/bundle:
|
41
57
|
ruby_version: << parameters.ruby_version >>
|
42
58
|
bundler_version: << parameters.bundler_version >>
|
43
|
-
project: active_encode
|
44
59
|
|
45
60
|
- samvera/rubocop
|
46
61
|
|
@@ -55,9 +70,6 @@ workflows:
|
|
55
70
|
- bundle_and_test:
|
56
71
|
name: "ruby2-5_rails5-2"
|
57
72
|
ruby_version: "2.5.5"
|
58
|
-
- bundle_and_test:
|
59
|
-
name: "ruby2-4_rails5-2"
|
60
|
-
ruby_version: "2.4.6"
|
61
73
|
- bundle_and_test:
|
62
74
|
name: "ruby2-6_rails5-1"
|
63
75
|
ruby_version: "2.6.3"
|
@@ -66,7 +78,3 @@ workflows:
|
|
66
78
|
name: "ruby2-5_rails5-1"
|
67
79
|
ruby_version: "2.5.5"
|
68
80
|
rails_version: "5.1.7"
|
69
|
-
- bundle_and_test:
|
70
|
-
name: "ruby2-4_rails5-1"
|
71
|
-
ruby_version: "2.4.6"
|
72
|
-
rails_version: "5.1.7"
|
data/.rubocop_todo.yml
CHANGED
@@ -40,6 +40,7 @@ Metrics/PerceivedComplexity:
|
|
40
40
|
RSpec/AnyInstance:
|
41
41
|
Exclude:
|
42
42
|
- 'spec/integration/ffmpeg_adapter_spec.rb'
|
43
|
+
- 'spec/integration/pass_through_adapter_spec.rb'
|
43
44
|
|
44
45
|
RSpec/ExampleLength:
|
45
46
|
Enabled: false
|
@@ -47,6 +48,7 @@ RSpec/ExampleLength:
|
|
47
48
|
RSpec/InstanceVariable:
|
48
49
|
Exclude:
|
49
50
|
- 'spec/integration/ffmpeg_adapter_spec.rb'
|
51
|
+
- 'spec/integration/pass_through_adapter_spec.rb'
|
50
52
|
|
51
53
|
RSpec/MessageSpies:
|
52
54
|
Exclude:
|
@@ -61,4 +63,5 @@ RSpec/VerifiedDoubles:
|
|
61
63
|
|
62
64
|
Style/GuardClause:
|
63
65
|
Exclude:
|
66
|
+
- 'lib/active_encode/engine_adapters/pass_through_adapter.rb'
|
64
67
|
- 'lib/file_locator.rb'
|
data/active_encode.gemspec
CHANGED
@@ -33,4 +33,7 @@ Gem::Specification.new do |spec|
|
|
33
33
|
spec.add_development_dependency "rspec-its"
|
34
34
|
spec.add_development_dependency 'rspec_junit_formatter'
|
35
35
|
spec.add_development_dependency "rspec-rails"
|
36
|
+
|
37
|
+
# Pin sprockets to < 4 so it works with ruby 2.5+
|
38
|
+
spec.add_dependency 'sprockets', '< 4'
|
36
39
|
end
|
data/lib/active_encode/base.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'active_encode/core'
|
3
3
|
require 'active_encode/engine_adapter'
|
4
|
+
require 'active_encode/errors'
|
4
5
|
require 'active_encode/status'
|
5
6
|
require 'active_encode/technical_metadata'
|
6
7
|
require 'active_encode/input'
|
@@ -18,6 +19,4 @@ module ActiveEncode #:nodoc:
|
|
18
19
|
include Callbacks
|
19
20
|
include GlobalID
|
20
21
|
end
|
21
|
-
|
22
|
-
class NotFound < RuntimeError; end
|
23
22
|
end
|
@@ -121,7 +121,7 @@ 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
127
|
def copy_to_input_bucket(input_url, bucket)
|
@@ -161,11 +161,12 @@ 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' : ''
|
@@ -173,7 +174,7 @@ module ActiveEncode
|
|
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
|
+
url: "s3://#{@pipeline.output_bucket}/#{job.output_key_prefix}#{joutput.key}#{extension}"
|
177
178
|
}
|
178
179
|
tech_md = convert_tech_metadata(joutput, preset).merge(additional_metadata)
|
179
180
|
|
@@ -1,13 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'fileutils'
|
3
3
|
require 'nokogiri'
|
4
|
+
require 'shellwords'
|
4
5
|
|
5
6
|
module ActiveEncode
|
6
7
|
module EngineAdapters
|
7
8
|
class FfmpegAdapter
|
8
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"
|
9
12
|
|
10
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
|
+
|
11
25
|
new_encode = ActiveEncode::Base.new(input_url, options)
|
12
26
|
new_encode.id = SecureRandom.uuid
|
13
27
|
new_encode.created_at = Time.now.utc
|
@@ -20,7 +34,7 @@ module ActiveEncode
|
|
20
34
|
FileUtils.mkdir_p working_path("outputs", new_encode.id)
|
21
35
|
|
22
36
|
# Extract technical metadata from input file
|
23
|
-
|
37
|
+
`#{MEDIAINFO_PATH} --Output=XML --LogFile=#{working_path("input_metadata", new_encode.id)} "#{input_url}"`
|
24
38
|
new_encode.input = build_input new_encode
|
25
39
|
|
26
40
|
if new_encode.input.duration.blank?
|
@@ -43,14 +57,20 @@ module ActiveEncode
|
|
43
57
|
|
44
58
|
# Run the ffmpeg command and save its pid
|
45
59
|
command = ffmpeg_command(input_url, new_encode.id, options)
|
46
|
-
pid = Process.spawn(command)
|
60
|
+
pid = Process.spawn(command, err: working_path('error.log', new_encode.id))
|
47
61
|
File.open(working_path("pid", new_encode.id), 'w') { |file| file.write pid }
|
48
62
|
new_encode.input.id = pid
|
49
63
|
|
50
|
-
# Prevent zombie process
|
51
|
-
Process.detach(pid)
|
52
|
-
|
53
64
|
new_encode
|
65
|
+
rescue StandardError => e
|
66
|
+
new_encode.state = :failed
|
67
|
+
new_encode.percent_complete = 1
|
68
|
+
new_encode.errors = [e.full_message]
|
69
|
+
write_errors new_encode
|
70
|
+
return new_encode
|
71
|
+
ensure
|
72
|
+
# Prevent zombie process
|
73
|
+
Process.detach(pid) if pid.present?
|
54
74
|
end
|
55
75
|
|
56
76
|
# Return encode object from file system
|
@@ -67,28 +87,21 @@ module ActiveEncode
|
|
67
87
|
pid = get_pid(id)
|
68
88
|
encode.input.id = pid if pid.present?
|
69
89
|
|
70
|
-
if File.file? working_path("error.log", id)
|
71
|
-
error = File.read working_path("error.log", id)
|
72
|
-
if error.present?
|
73
|
-
encode.state = :failed
|
74
|
-
encode.errors = [error]
|
75
|
-
|
76
|
-
return encode
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
encode.errors = []
|
81
|
-
|
82
90
|
encode.current_operations = []
|
83
91
|
encode.created_at, encode.updated_at = get_times encode.id
|
84
|
-
|
85
|
-
if
|
92
|
+
encode.errors = read_errors(id)
|
93
|
+
if encode.errors.present?
|
94
|
+
encode.state = :failed
|
95
|
+
elsif running? pid
|
86
96
|
encode.state = :running
|
87
97
|
encode.current_operations = ["transcoding"]
|
88
98
|
elsif progress_ended?(encode.id) && encode.percent_complete == 100
|
89
99
|
encode.state = :completed
|
90
|
-
elsif encode.
|
100
|
+
elsif cancelled? encode.id
|
91
101
|
encode.state = :cancelled
|
102
|
+
elsif encode.percent_complete < 100
|
103
|
+
encode.errors << "Encoding has completed but the output duration is shorter than the input"
|
104
|
+
encode.state = :failed
|
92
105
|
end
|
93
106
|
|
94
107
|
encode.output = build_outputs encode if encode.completed?
|
@@ -98,10 +111,30 @@ module ActiveEncode
|
|
98
111
|
|
99
112
|
# Cancel ongoing encode using pid file
|
100
113
|
def cancel(id)
|
101
|
-
|
102
|
-
|
114
|
+
encode = find id
|
115
|
+
if encode.running?
|
116
|
+
pid = get_pid(id)
|
117
|
+
|
118
|
+
IO.popen("ps -ef | grep #{pid}") do |pipe|
|
119
|
+
child_pids = pipe.readlines.map do |line|
|
120
|
+
parts = line.split(/\s+/)
|
121
|
+
parts[1] if parts[2] == pid.to_s && parts[1] != pipe.pid.to_s
|
122
|
+
end.compact
|
123
|
+
|
124
|
+
child_pids.each do |cpid|
|
125
|
+
Process.kill 'SIGTERM', cpid.to_i
|
126
|
+
end
|
127
|
+
end
|
103
128
|
|
104
|
-
|
129
|
+
Process.kill 'SIGTERM', pid.to_i
|
130
|
+
File.write(working_path("cancelled", id), "")
|
131
|
+
encode = find id
|
132
|
+
end
|
133
|
+
encode
|
134
|
+
rescue Errno::ESRCH
|
135
|
+
raise NotRunningError
|
136
|
+
rescue StandardError
|
137
|
+
raise CancelError
|
105
138
|
end
|
106
139
|
|
107
140
|
private
|
@@ -122,6 +155,16 @@ module ActiveEncode
|
|
122
155
|
File.write(working_path("error.log", encode.id), encode.errors.join("\n"))
|
123
156
|
end
|
124
157
|
|
158
|
+
def read_errors(id)
|
159
|
+
err_path = working_path("error.log", id)
|
160
|
+
error = File.read(err_path) if File.file? err_path
|
161
|
+
if error.present?
|
162
|
+
[error]
|
163
|
+
else
|
164
|
+
[]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
125
168
|
def build_input(encode)
|
126
169
|
input = ActiveEncode::Input.new
|
127
170
|
metadata = get_tech_metadata(working_path("input_metadata", encode.id))
|
@@ -140,16 +183,15 @@ module ActiveEncode
|
|
140
183
|
Dir["#{File.absolute_path(working_path('outputs', id))}/*"].each do |file_path|
|
141
184
|
output = ActiveEncode::Output.new
|
142
185
|
output.url = "file://#{file_path}"
|
143
|
-
|
144
|
-
|
145
|
-
output.label = file_path[/#{Regexp.quote(original_filename)}\-(.*?)#{Regexp.quote(File.extname(file_path))}$/, 1]
|
186
|
+
sanitized_filename = sanitize_base encode.input.url
|
187
|
+
output.label = file_path[/#{Regexp.quote(sanitized_filename)}\-(.*?)#{Regexp.quote(File.extname(file_path))}$/, 1]
|
146
188
|
output.id = "#{encode.input.id}-#{output.label}"
|
147
189
|
output.created_at = encode.created_at
|
148
190
|
output.updated_at = File.mtime file_path
|
149
191
|
|
150
192
|
# Extract technical metadata from output file
|
151
193
|
metadata_path = working_path("output_metadata-#{output.label}", id)
|
152
|
-
|
194
|
+
`#{MEDIAINFO_PATH} --Output=XML --LogFile=#{metadata_path} #{output.url}` unless File.file? metadata_path
|
153
195
|
output.assign_tech_metadata(get_tech_metadata(metadata_path))
|
154
196
|
|
155
197
|
outputs << output
|
@@ -160,11 +202,19 @@ module ActiveEncode
|
|
160
202
|
|
161
203
|
def ffmpeg_command(input_url, id, opts)
|
162
204
|
output_opt = opts[:outputs].collect do |output|
|
163
|
-
|
205
|
+
sanitized_filename = sanitize_base input_url
|
206
|
+
file_name = "outputs/#{sanitized_filename}-#{output[:label]}.#{output[:extension]}"
|
164
207
|
" #{output[:ffmpeg_opt]} #{working_path(file_name, id)}"
|
165
208
|
end.join(" ")
|
209
|
+
"#{FFMPEG_PATH} -y -loglevel error -progress #{working_path('progress', id)} -i \"#{input_url}\" #{output_opt}"
|
210
|
+
end
|
166
211
|
|
167
|
-
|
212
|
+
def sanitize_base(input_url)
|
213
|
+
if input_url.is_a? URI::HTTP
|
214
|
+
File.basename(input_url.path, File.extname(input_url.path))
|
215
|
+
else
|
216
|
+
File.basename(input_url, File.extname(input_url)).gsub(/[^0-9A-Za-z.\-]/, '_')
|
217
|
+
end
|
168
218
|
end
|
169
219
|
|
170
220
|
def get_pid(id)
|
@@ -188,10 +238,16 @@ module ActiveEncode
|
|
188
238
|
1
|
189
239
|
else
|
190
240
|
progress_in_milliseconds = progress_value("out_time_ms=", data).to_i / 1000.0
|
191
|
-
(progress_in_milliseconds / encode.input.duration * 100).
|
241
|
+
output = (progress_in_milliseconds / encode.input.duration * 100).ceil
|
242
|
+
return 100 if output > 100
|
243
|
+
output
|
192
244
|
end
|
193
245
|
end
|
194
246
|
|
247
|
+
def cancelled?(id)
|
248
|
+
File.exist? working_path("cancelled", id)
|
249
|
+
end
|
250
|
+
|
195
251
|
def read_progress(id)
|
196
252
|
File.read working_path("progress", id) if File.file? working_path("progress", id)
|
197
253
|
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'fileutils'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'file_locator'
|
6
|
+
|
7
|
+
# PassThroughAdapter accepts an input file url and a number of derivative urls in the options
|
8
|
+
# E.g. `create(input, outputs: [{ label: 'low', url: 'file:///derivatives/low.mp4' }, { label: 'high', url: 'file:///derivatives/high.mp4' }])`
|
9
|
+
# This adapter mirrors the ffmpeg adapter but differs in a few ways:
|
10
|
+
# 1. It starts by copying the derivative files to the work directory
|
11
|
+
# 2. It runs Mediainfo on the input and output files and skips ffmpeg
|
12
|
+
# 3. All work is done in the create method so it's status is always completed or failed
|
13
|
+
module ActiveEncode
|
14
|
+
module EngineAdapters
|
15
|
+
class PassThroughAdapter
|
16
|
+
WORK_DIR = ENV["ENCODE_WORK_DIR"] || "encodes" # Should read from config
|
17
|
+
MEDIAINFO_PATH = ENV["MEDIAINFO_PATH"] || "mediainfo"
|
18
|
+
|
19
|
+
def create(input_url, options = {})
|
20
|
+
# Decode file uris for ffmpeg (mediainfo works either way)
|
21
|
+
input_url = URI.decode(input_url) if input_url.starts_with? "file:///"
|
22
|
+
|
23
|
+
new_encode = ActiveEncode::Base.new(input_url, options)
|
24
|
+
new_encode.id = SecureRandom.uuid
|
25
|
+
new_encode.current_operations = []
|
26
|
+
new_encode.output = []
|
27
|
+
|
28
|
+
# Create a working directory that holds all output files related to the encode
|
29
|
+
FileUtils.mkdir_p working_path("", new_encode.id)
|
30
|
+
FileUtils.mkdir_p working_path("outputs", new_encode.id)
|
31
|
+
|
32
|
+
# Extract technical metadata from input file
|
33
|
+
`#{MEDIAINFO_PATH} --Output=XML --LogFile=#{working_path("input_metadata", new_encode.id)} #{input_url.shellescape}`
|
34
|
+
new_encode.input = build_input new_encode
|
35
|
+
new_encode.input.id = new_encode.id
|
36
|
+
new_encode.created_at, new_encode.updated_at = get_times new_encode.id
|
37
|
+
|
38
|
+
if new_encode.input.duration.blank?
|
39
|
+
new_encode.state = :failed
|
40
|
+
new_encode.percent_complete = 1
|
41
|
+
|
42
|
+
new_encode.errors = if new_encode.input.file_size.blank?
|
43
|
+
["#{input_url} does not exist or is not accessible"]
|
44
|
+
else
|
45
|
+
["Error inspecting input: #{input_url}"]
|
46
|
+
end
|
47
|
+
|
48
|
+
write_errors new_encode
|
49
|
+
return new_encode
|
50
|
+
end
|
51
|
+
|
52
|
+
# For saving filename to label map used to find the label when building outputs
|
53
|
+
filename_label_hash = {}
|
54
|
+
|
55
|
+
# Copy derivatives to work directory
|
56
|
+
options[:outputs].each do |opt|
|
57
|
+
url = opt[:url]
|
58
|
+
output_path = working_path("outputs/#{sanitize_base opt[:url]}#{File.extname opt[:url]}", new_encode.id)
|
59
|
+
FileUtils.cp FileLocator.new(url).location, output_path
|
60
|
+
filename_label_hash[output_path] = opt[:label]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Write filename-to-label map so we can retrieve them on build_output
|
64
|
+
File.write working_path("filename_label.yml", new_encode.id), filename_label_hash.to_yaml
|
65
|
+
|
66
|
+
new_encode.percent_complete = 1
|
67
|
+
new_encode.state = :running
|
68
|
+
new_encode.errors = []
|
69
|
+
|
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
|
+
end
|
78
|
+
|
79
|
+
# Return encode object from file system
|
80
|
+
def find(id, opts = {})
|
81
|
+
encode_class = opts[:cast]
|
82
|
+
encode_class ||= ActiveEncode::Base
|
83
|
+
encode = encode_class.new(nil, opts)
|
84
|
+
encode.id = id
|
85
|
+
encode.created_at, encode.updated_at = get_times encode.id
|
86
|
+
encode.input = build_input encode
|
87
|
+
encode.input.id = encode.id
|
88
|
+
encode.output = []
|
89
|
+
encode.current_operations = []
|
90
|
+
|
91
|
+
encode.errors = read_errors(id)
|
92
|
+
if encode.errors.present?
|
93
|
+
encode.state = :failed
|
94
|
+
encode.percent_complete = 1
|
95
|
+
elsif cancelled?(id)
|
96
|
+
encode.state = :cancelled
|
97
|
+
encode.percent_complete = 1
|
98
|
+
elsif completed?(id)
|
99
|
+
encode.state = :completed
|
100
|
+
encode.percent_complete = 100
|
101
|
+
else
|
102
|
+
encode.output = build_outputs encode
|
103
|
+
encode.state = :completed
|
104
|
+
encode.percent_complete = 100
|
105
|
+
end
|
106
|
+
|
107
|
+
encode
|
108
|
+
rescue StandardError => e
|
109
|
+
encode.state = :failed
|
110
|
+
encode.percent_complete = 1
|
111
|
+
encode.errors = [e.full_message]
|
112
|
+
write_errors encode
|
113
|
+
return encode
|
114
|
+
end
|
115
|
+
|
116
|
+
# Cancel ongoing encode using pid file
|
117
|
+
def cancel(id)
|
118
|
+
# Check for errors and if not then create cancelled file else raise CancelError?
|
119
|
+
if running?(id)
|
120
|
+
File.write(working_path("cancelled", id), "")
|
121
|
+
find id
|
122
|
+
else
|
123
|
+
raise CancelError
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def running?(id)
|
130
|
+
!cancelled?(id) || !failed?(id) || !completed?(id)
|
131
|
+
end
|
132
|
+
|
133
|
+
def cancelled?(id)
|
134
|
+
File.exist? working_path("cancelled", id)
|
135
|
+
end
|
136
|
+
|
137
|
+
def failed?(id)
|
138
|
+
read_errors(id).present?
|
139
|
+
end
|
140
|
+
|
141
|
+
def completed?(id)
|
142
|
+
File.exist? working_path("completed", id)
|
143
|
+
end
|
144
|
+
|
145
|
+
def get_times(id)
|
146
|
+
updated_at = if File.file? working_path("completed", id)
|
147
|
+
File.mtime(working_path("completed", id))
|
148
|
+
elsif File.file? working_path("cancelled", id)
|
149
|
+
File.mtime(working_path("cancelled", id))
|
150
|
+
elsif File.file? working_path("error.log", id)
|
151
|
+
File.mtime(working_path("error.log", id))
|
152
|
+
else
|
153
|
+
File.mtime(working_path("input_metadata", id))
|
154
|
+
end
|
155
|
+
|
156
|
+
[File.mtime(working_path("input_metadata", id)), updated_at]
|
157
|
+
end
|
158
|
+
|
159
|
+
def write_errors(encode)
|
160
|
+
File.write(working_path("error.log", encode.id), encode.errors.join("\n"))
|
161
|
+
end
|
162
|
+
|
163
|
+
def read_errors(id)
|
164
|
+
err_path = working_path("error.log", id)
|
165
|
+
error = File.read(err_path) if File.file? err_path
|
166
|
+
if error.present?
|
167
|
+
[error]
|
168
|
+
else
|
169
|
+
[]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def build_input(encode)
|
174
|
+
input = ActiveEncode::Input.new
|
175
|
+
metadata = get_tech_metadata(working_path("input_metadata", encode.id))
|
176
|
+
input.url = metadata[:url]
|
177
|
+
input.assign_tech_metadata(metadata)
|
178
|
+
created_at = File.mtime(working_path("input_metadata", encode.id))
|
179
|
+
input.created_at = created_at
|
180
|
+
input.updated_at = created_at
|
181
|
+
|
182
|
+
input
|
183
|
+
end
|
184
|
+
|
185
|
+
def build_outputs(encode)
|
186
|
+
id = encode.id
|
187
|
+
outputs = []
|
188
|
+
filename_label_hash = YAML.safe_load(File.read(working_path("filename_label.yml", id))) if File.exist?(working_path("filename_label.yml", id))
|
189
|
+
Dir["#{File.absolute_path(working_path('outputs', id))}/*"].each do |file_path|
|
190
|
+
output = ActiveEncode::Output.new
|
191
|
+
output.url = "file://#{file_path}"
|
192
|
+
output.label = filename_label_hash[file_path] if filename_label_hash
|
193
|
+
output.id = "#{encode.input.id}-#{output.label}"
|
194
|
+
output.created_at = encode.created_at
|
195
|
+
output.updated_at = File.mtime file_path
|
196
|
+
|
197
|
+
# Extract technical metadata from output file
|
198
|
+
metadata_path = working_path("output_metadata-#{output.label}", id)
|
199
|
+
`#{MEDIAINFO_PATH} --Output=XML --LogFile=#{metadata_path} #{output.url}` unless File.file? metadata_path
|
200
|
+
output.assign_tech_metadata(get_tech_metadata(metadata_path))
|
201
|
+
|
202
|
+
outputs << output
|
203
|
+
end
|
204
|
+
File.write(working_path("completed", id), "")
|
205
|
+
|
206
|
+
outputs
|
207
|
+
end
|
208
|
+
|
209
|
+
def sanitize_base(input_url)
|
210
|
+
File.basename(input_url, File.extname(input_url)).gsub(/[^0-9A-Za-z.\-]/, '_')
|
211
|
+
end
|
212
|
+
|
213
|
+
def working_path(path, id)
|
214
|
+
File.join(WORK_DIR, id, path)
|
215
|
+
end
|
216
|
+
|
217
|
+
def get_tech_metadata(file_path)
|
218
|
+
doc = Nokogiri::XML File.read(file_path)
|
219
|
+
doc.remove_namespaces!
|
220
|
+
duration = get_xpath_text(doc, '//Duration/text()', :to_f)
|
221
|
+
duration *= 1000 unless duration.nil? # Convert to milliseconds
|
222
|
+
{ url: get_xpath_text(doc, '//media/@ref', :to_s),
|
223
|
+
width: get_xpath_text(doc, '//Width/text()', :to_f),
|
224
|
+
height: get_xpath_text(doc, '//Height/text()', :to_f),
|
225
|
+
frame_rate: get_xpath_text(doc, '//FrameRate/text()', :to_f),
|
226
|
+
duration: duration,
|
227
|
+
file_size: get_xpath_text(doc, '//FileSize/text()', :to_i),
|
228
|
+
audio_codec: get_xpath_text(doc, '//track[@type="Audio"]/CodecID/text()', :to_s),
|
229
|
+
audio_bitrate: get_xpath_text(doc, '//track[@type="Audio"]/BitRate/text()', :to_i),
|
230
|
+
video_codec: get_xpath_text(doc, '//track[@type="Video"]/CodecID/text()', :to_s),
|
231
|
+
video_bitrate: get_xpath_text(doc, '//track[@type="Video"]/BitRate/text()', :to_i) }
|
232
|
+
end
|
233
|
+
|
234
|
+
def get_xpath_text(doc, xpath, cast_method)
|
235
|
+
doc.xpath(xpath).first&.text&.send(cast_method)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|