active_encode 0.5.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +80 -0
  3. data/.rubocop.yml +9 -70
  4. data/.rubocop_todo.yml +68 -0
  5. data/Gemfile +5 -4
  6. data/README.md +69 -0
  7. data/active_encode.gemspec +12 -3
  8. data/app/controllers/active_encode/encode_record_controller.rb +1 -0
  9. data/app/jobs/active_encode/polling_job.rb +1 -1
  10. data/app/models/active_encode/encode_record.rb +1 -0
  11. data/db/migrate/20180822021048_create_active_encode_encode_records.rb +1 -0
  12. data/db/migrate/20190702153755_add_create_options_to_active_encode_encode_records.rb +6 -0
  13. data/db/migrate/20190712174821_add_progress_to_active_encode_encode_records.rb +6 -0
  14. data/lib/active_encode.rb +1 -0
  15. data/lib/active_encode/base.rb +2 -2
  16. data/lib/active_encode/callbacks.rb +1 -0
  17. data/lib/active_encode/core.rb +4 -3
  18. data/lib/active_encode/engine.rb +1 -0
  19. data/lib/active_encode/engine_adapter.rb +1 -0
  20. data/lib/active_encode/engine_adapters.rb +4 -1
  21. data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +31 -29
  22. data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +138 -87
  23. data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +5 -4
  24. data/lib/active_encode/engine_adapters/media_convert_adapter.rb +399 -0
  25. data/lib/active_encode/engine_adapters/media_convert_output.rb +104 -0
  26. data/lib/active_encode/engine_adapters/pass_through_adapter.rb +239 -0
  27. data/lib/active_encode/engine_adapters/test_adapter.rb +5 -4
  28. data/lib/active_encode/engine_adapters/zencoder_adapter.rb +3 -2
  29. data/lib/active_encode/errors.rb +6 -0
  30. data/lib/active_encode/global_id.rb +2 -1
  31. data/lib/active_encode/input.rb +3 -2
  32. data/lib/active_encode/output.rb +3 -2
  33. data/lib/active_encode/persistence.rb +11 -5
  34. data/lib/active_encode/polling.rb +3 -2
  35. data/lib/active_encode/spec/shared_specs.rb +2 -0
  36. data/{spec/shared_specs/engine_adapter_specs.rb → lib/active_encode/spec/shared_specs/engine_adapter.rb} +37 -38
  37. data/lib/active_encode/status.rb +1 -0
  38. data/lib/active_encode/technical_metadata.rb +3 -2
  39. data/lib/active_encode/version.rb +2 -1
  40. data/lib/file_locator.rb +8 -9
  41. data/spec/controllers/encode_record_controller_spec.rb +4 -3
  42. data/spec/fixtures/ffmpeg/cancelled-id/cancelled +0 -0
  43. data/spec/fixtures/file with space.low.mp4 +0 -0
  44. data/spec/fixtures/file with space.mp4 +0 -0
  45. data/spec/fixtures/fireworks.low.mp4 +0 -0
  46. data/spec/fixtures/media_convert/endpoints.json +1 -0
  47. data/spec/fixtures/media_convert/job_canceled.json +412 -0
  48. data/spec/fixtures/media_convert/job_canceling.json +1 -0
  49. data/spec/fixtures/media_convert/job_completed.json +359 -0
  50. data/spec/fixtures/media_convert/job_completed_detail.json +1 -0
  51. data/spec/fixtures/media_convert/job_completed_detail_query.json +1 -0
  52. data/spec/fixtures/media_convert/job_completed_empty_detail.json +1 -0
  53. data/spec/fixtures/media_convert/job_created.json +408 -0
  54. data/spec/fixtures/media_convert/job_failed.json +406 -0
  55. data/spec/fixtures/media_convert/job_progressing.json +414 -0
  56. data/spec/fixtures/pass_through/cancelled-id/cancelled +0 -0
  57. data/spec/fixtures/pass_through/cancelled-id/input_metadata +90 -0
  58. data/spec/fixtures/pass_through/completed-id/completed +0 -0
  59. data/spec/fixtures/pass_through/completed-id/input_metadata +102 -0
  60. data/spec/fixtures/pass_through/completed-id/output_metadata-high +90 -0
  61. data/spec/fixtures/pass_through/completed-id/output_metadata-low +90 -0
  62. data/spec/fixtures/pass_through/completed-id/video-high.mp4 +0 -0
  63. data/spec/fixtures/pass_through/completed-id/video-low.mp4 +0 -0
  64. data/spec/fixtures/pass_through/failed-id/error.log +1 -0
  65. data/spec/fixtures/pass_through/failed-id/input_metadata +90 -0
  66. data/spec/fixtures/pass_through/running-id/input_metadata +90 -0
  67. data/spec/integration/elastic_transcoder_adapter_spec.rb +30 -30
  68. data/spec/integration/ffmpeg_adapter_spec.rb +93 -25
  69. data/spec/integration/matterhorn_adapter_spec.rb +45 -44
  70. data/spec/integration/media_convert_adapter_spec.rb +152 -0
  71. data/spec/integration/pass_through_adapter_spec.rb +151 -0
  72. data/spec/integration/zencoder_adapter_spec.rb +210 -209
  73. data/spec/rails_helper.rb +1 -0
  74. data/spec/routing/encode_record_controller_routing_spec.rb +1 -0
  75. data/spec/spec_helper.rb +2 -2
  76. data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -12
  77. data/spec/units/callbacks_spec.rb +3 -2
  78. data/spec/units/core_spec.rb +26 -25
  79. data/spec/units/engine_adapter_spec.rb +1 -0
  80. data/spec/units/file_locator_spec.rb +20 -19
  81. data/spec/units/global_id_spec.rb +12 -11
  82. data/spec/units/input_spec.rb +8 -5
  83. data/spec/units/output_spec.rb +8 -5
  84. data/spec/units/persistence_spec.rb +15 -11
  85. data/spec/units/polling_job_spec.rb +7 -6
  86. data/spec/units/polling_spec.rb +1 -0
  87. data/spec/units/status_spec.rb +3 -3
  88. metadata +158 -14
  89. data/.travis.yml +0 -19
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_model/callbacks'
2
3
 
3
4
  module ActiveEncode
@@ -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(self.input.url, self.options))
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(self.id))
60
+ merge!(self.class.engine_adapter.cancel(id))
60
61
  end
61
62
  end
62
63
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails'
2
3
 
3
4
  module ActiveEncode
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_encode/engine_adapters'
2
3
  require 'active_support/core_ext/class/attribute'
3
4
  require 'active_support/core_ext/string/inflections'
@@ -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'.freeze
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, opts = {})
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
- 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
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
- encode.output = convert_output(job)
68
- encode.errors = job.outputs.select { |o| o.status == "Error" }.collect(&:status_detail).compact
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 input_url, 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 input_url, source_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 > 15728640) # 15.megabytes
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 input_url, source_bucket
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
- client.read_preset(id: id).preset
164
+ @presets ||= {}
165
+ @presets[id] ||= client.read_preset(id: id).preset
165
166
  end
166
167
 
167
168
  def convert_output(job)
168
- pipeline = client.read_pipeline(id: job.pipeline_id).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
- tech_md = convert_tech_metadata(joutput, preset).merge({
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.new
13
- new_encode.updated_at = Time.new
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
- `mediainfo --Output=XML --LogFile=#{working_path("input_metadata", new_encode.id)} #{input_url}`
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
- new_encode.errors = ["#{input_url} does not exist or is not accessible"]
31
- else
32
- new_encode.errors = ["Error inspecting input: #{input_url}"]
33
- end
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
- encode = ActiveEncode::Base.new(nil, opts)
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 running? pid
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
- encode.state = :completed
105
+ encode.state = :completed
106
+ elsif cancelled? encode.id
107
+ encode.state = :cancelled
88
108
  elsif encode.percent_complete < 100
89
- encode.state = :cancelled
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
- pid = get_pid(id)
100
- Process.kill 'SIGTERM', pid.to_i
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
- find id
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 id
148
+ def get_times(id)
108
149
  updated_at = if File.file? working_path("progress", id)
109
- File.mtime(working_path("progress", id))
110
- elsif File.file? working_path("error.log", id)
111
- File.mtime(working_path("error.log", id))
112
- else
113
- File.mtime(working_path("input_metadata", id))
114
- end
115
-
116
- return File.mtime(working_path("input_metadata", id)), updated_at
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 encode
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 build_input encode
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 encode
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
- original_extension = File.extname(encode.input.url)
142
- original_filename = File.basename(encode.input.url).chomp(original_extension)
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
- `mediainfo --Output=XML --LogFile=#{metadata_path} #{output.url}` unless File.file? metadata_path
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
- file_name = "outputs/#{File.basename(input_url, File.extname(input_url))}-#{output[:label]}.#{output[:extension]}"
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
- "ffmpeg -y -loglevel error -progress #{working_path("progress", id)} -i #{input_url} #{output_opt} > #{working_path("error.log", id)} 2>&1"
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 get_pid(id)
169
- if File.file? working_path("pid", id)
170
- File.read(working_path("pid", id)).remove("\n")
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
- nil
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
- begin
182
- Process.getpgid pid.to_i
183
- true
184
- rescue Errno::ESRCH
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 encode
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).round
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 read_progress id
200
- if File.file? working_path("progress", id)
201
- File.read working_path("progress", id)
202
- else
203
- nil
204
- end
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? id
265
+ def progress_ended?(id)
208
266
  "end" == progress_value("progress=", read_progress(id))
209
267
  end
210
268
 
211
- def progress_value key, data
212
- if data.present? && key.present?
213
- ri = data.rindex(key) + key.length
214
- data[ri..data.index("\n", ri)-1]
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 file_path
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 = duration * 1000 unless duration.nil? # Convert to milliseconds
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 doc, xpath, cast_method
238
- if doc.xpath(xpath).first
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