active_encode 0.2 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +9 -0
  3. data/README.md +1 -16
  4. data/active_encode.gemspec +4 -4
  5. data/app/jobs/active_encode/polling_job.rb +4 -4
  6. data/lib/active_encode/callbacks.rb +2 -21
  7. data/lib/active_encode/core.rb +1 -35
  8. data/lib/active_encode/engine_adapters.rb +1 -3
  9. data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +32 -28
  10. data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +243 -0
  11. data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +64 -97
  12. data/lib/active_encode/engine_adapters/test_adapter.rb +0 -12
  13. data/lib/active_encode/engine_adapters/zencoder_adapter.rb +53 -44
  14. data/lib/active_encode/input.rb +6 -0
  15. data/lib/active_encode/output.rb +7 -0
  16. data/lib/active_encode/persistence.rb +1 -1
  17. data/lib/active_encode/polling.rb +2 -2
  18. data/lib/active_encode/status.rb +0 -3
  19. data/lib/active_encode/technical_metadata.rb +7 -0
  20. data/lib/active_encode/version.rb +1 -1
  21. data/spec/fixtures/ffmpeg/cancelled-id/error.log +0 -0
  22. data/spec/fixtures/ffmpeg/cancelled-id/input_metadata +90 -0
  23. data/spec/fixtures/ffmpeg/cancelled-id/pid +1 -0
  24. data/spec/fixtures/ffmpeg/cancelled-id/progress +11 -0
  25. data/spec/fixtures/ffmpeg/completed-id/error.log +0 -0
  26. data/spec/fixtures/ffmpeg/completed-id/input_metadata +102 -0
  27. data/spec/fixtures/ffmpeg/completed-id/output_metadata-high +90 -0
  28. data/spec/fixtures/ffmpeg/completed-id/output_metadata-low +90 -0
  29. data/spec/fixtures/ffmpeg/completed-id/pid +1 -0
  30. data/spec/fixtures/ffmpeg/completed-id/progress +11 -0
  31. data/spec/fixtures/ffmpeg/completed-id/video-high.mp4 +0 -0
  32. data/spec/fixtures/ffmpeg/completed-id/video-low.mp4 +0 -0
  33. data/spec/fixtures/ffmpeg/failed-id/error.log +1 -0
  34. data/spec/fixtures/ffmpeg/failed-id/input_metadata +90 -0
  35. data/spec/fixtures/ffmpeg/failed-id/pid +1 -0
  36. data/spec/fixtures/ffmpeg/failed-id/progress +11 -0
  37. data/spec/fixtures/ffmpeg/running-id/error.log +0 -0
  38. data/spec/fixtures/ffmpeg/running-id/input_metadata +90 -0
  39. data/spec/fixtures/ffmpeg/running-id/pid +1 -0
  40. data/spec/fixtures/ffmpeg/running-id/progress +11 -0
  41. data/spec/fixtures/fireworks.mp4 +0 -0
  42. data/spec/integration/elastic_transcoder_adapter_spec.rb +21 -12
  43. data/spec/integration/ffmpeg_adapter_spec.rb +120 -0
  44. data/spec/integration/matterhorn_adapter_spec.rb +30 -59
  45. data/spec/integration/zencoder_adapter_spec.rb +242 -22
  46. data/spec/shared_specs/engine_adapter_specs.rb +116 -16
  47. data/spec/units/core_spec.rb +31 -0
  48. data/spec/units/input_spec.rb +25 -0
  49. data/spec/units/output_spec.rb +28 -1
  50. data/spec/units/polling_job_spec.rb +10 -10
  51. metadata +51 -11
  52. data/lib/active_encode/engine_adapters/active_job_adapter.rb +0 -21
  53. data/lib/active_encode/engine_adapters/inline_adapter.rb +0 -47
  54. data/lib/active_encode/engine_adapters/shingoncoder_adapter.rb +0 -63
  55. data/spec/integration/shingoncoder_adapter_spec.rb +0 -170
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3346c9f9ae44a17bf60b9f71dbf6a3bd8b951883
4
- data.tar.gz: 8f228d1184a1f61629bbed814176d131bc98de40
3
+ metadata.gz: 6e507e3110f65efc706aed54986fd98043fb0911
4
+ data.tar.gz: 0a5a69eabd90c026e23258156f2d4f93a136a4fd
5
5
  SHA512:
6
- metadata.gz: 47fde2ffc94ad11ef0b8ab3af29672f3c8707128d887d2dd48249f0e13346b66b5450434ab1d79f1b840bf2ac695c05f5cd6f1ef11ce697d16bbd63193a5ad8e
7
- data.tar.gz: 74073edc357c19e84fb51c80044684dceae704dcb8bef6061a49b62b269c78aa60fb47563db691d00fe798e76bfd7af88467290a4c135438beff51efbc2989b7
6
+ metadata.gz: 8fcd0939d74b37b03a61f93fc37baa2960bf2591b1e11430e07e816439b5102d66d71ee422ad87dcbab3da0020e2e7a01400f0730f7ed28b3ce3a46a546b0acc
7
+ data.tar.gz: b0e11f988144fce3220a7a551f0a1cb9c66f832023e89a1e989caf942c666932daddc60ca9b778b444b5c27de39827e11f0e8eb7a2fecf9304b65c1cf6d12c6e
data/.travis.yml CHANGED
@@ -1,5 +1,14 @@
1
1
  language: ruby
2
2
  cache: bundler
3
+ before_install:
4
+ # - sudo apt-get install mediainfo
5
+ - sudo apt-get install libmms0
6
+ - wget https://mediaarea.net/download/binary/libzen0/0.4.37/libzen0_0.4.37-1_amd64.xUbuntu_14.04.deb
7
+ - sudo dpkg -i libzen0_0.4.37-1_amd64.xUbuntu_14.04.deb
8
+ - wget https://mediaarea.net/download/binary/libmediainfo0/18.08.1/libmediainfo0_18.08.1-1_amd64.xUbuntu_14.04.deb
9
+ - sudo dpkg -i libmediainfo0_18.08.1-1_amd64.xUbuntu_14.04.deb
10
+ - wget https://mediaarea.net/download/binary/mediainfo/18.08.1/mediainfo_18.08.1-1_amd64.xUbuntu_14.04.deb
11
+ - sudo dpkg -i mediainfo_18.08.1-1_amd64.xUbuntu_14.04.deb
3
12
  sudo: false
4
13
  rvm:
5
14
  - 2.3.7
data/README.md CHANGED
@@ -52,20 +52,7 @@ encode.cancel!
52
52
  encode.cancelled? # true
53
53
  ```
54
54
 
55
- > `#purge!` and `#remove_output!` and the following documentation have been deprecated and will be removed in ActiveEncode 0.3.
56
-
57
- If the encoding job should be deleted, call purge:
58
- ```ruby
59
- encode.purge!
60
- ```
61
-
62
- Purge will attempt to remove all outputs that have been generated. It is also possible to remove only a single output using its id:
63
-
64
- ```ruby
65
- encode.remove_output! 'track-9'
66
- ```
67
-
68
- An encoding job is meant to be the record of the work of the encoding engine and not the current state of the outputs. Therefore removing outputs will not be reflected in the encoding job.
55
+ An encoding job is meant to be the record of the work of the encoding engine and not the current state of the outputs. Therefore removed outputs will not be reflected in the encoding job.
69
56
 
70
57
  ### Custom jobs
71
58
 
@@ -94,8 +81,6 @@ Engine adapters are shims between ActiveEncode and the back end encoding service
94
81
  | Zencoder | X | X | X | | |
95
82
  | Matterhorn | X | X | X | X | X |
96
83
 
97
- > The Inline and Shingoncoder adapters are deprecated and will be removed in ActiveEncode 0.3.
98
-
99
84
  ## Contributing
100
85
 
101
86
  1. Fork it ( https://github.com/projecthydra-labs/active_encode/fork )
@@ -7,12 +7,12 @@ require 'active_encode/version'
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "active_encode"
9
9
  spec.version = ActiveEncode::VERSION
10
- spec.authors = ["Michael Klein, Chris Colvard"]
11
- spec.email = ["mbklein@gmail.com, chris.colvard@gmail.com"]
10
+ spec.authors = ["Michael Klein, Chris Colvard, Phuong Dinh"]
11
+ spec.email = ["mbklein@gmail.com, chris.colvard@gmail.com, phuongdh@gmail.com"]
12
12
  spec.summary = 'Declare encode job classes that can be run by a variety of encoding services'
13
13
  spec.description = 'This gem serves as the basis for the interface between a Ruby (Rails) application and a provider of transcoding services such as Opencast Matterhorn, Zencoder, and Amazon Elastic Transcoder.'
14
- spec.homepage = ""
15
- spec.license = "MIT"
14
+ spec.homepage = "https://github.com/samvera-labs/active_encode"
15
+ spec.license = "Apache-2.0"
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0")
18
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
@@ -4,12 +4,12 @@ module ActiveEncode
4
4
  def perform(encode)
5
5
  encode.run_callbacks(:status_update) { encode }
6
6
  case encode.state
7
- when :error
8
- encode.run_callbacks(:error) { encode }
7
+ when :failed
8
+ encode.run_callbacks(:failed) { encode }
9
9
  when :cancelled
10
10
  encode.run_callbacks(:cancelled) { encode }
11
- when :complete
12
- encode.run_callbacks(:complete) { encode }
11
+ when :completed
12
+ encode.run_callbacks(:completed) { encode }
13
13
  when :running
14
14
  ActiveEncode::PollingJob.set(wait: ActiveEncode::Polling::POLLING_WAIT_TIME).perform_later(encode)
15
15
  else # other states are illegal and ignored
@@ -14,39 +14,20 @@ module ActiveEncode
14
14
  # * <tt>before_cancel</tt>
15
15
  # * <tt>around_cancel</tt>
16
16
  # * <tt>after_cancel</tt>
17
- # * <tt>before_purge</tt>
18
- # * <tt>around_purge</tt>
19
- # * <tt>after_purge</tt>
20
17
  #
21
18
  module Callbacks
22
19
  extend ActiveSupport::Concern
23
20
 
24
21
  CALLBACKS = [
25
22
  :after_find, :after_reload, :before_create, :around_create,
26
- :after_create, :before_cancel, :around_cancel, :after_cancel,
27
- :before_purge, :around_purge, :after_purge
23
+ :after_create, :before_cancel, :around_cancel, :after_cancel
28
24
  ].freeze
29
25
 
30
26
  included do
31
27
  extend ActiveModel::Callbacks
32
28
 
33
29
  define_model_callbacks :find, :reload, only: :after
34
- define_model_callbacks :create, :cancel, :purge
35
-
36
- def self.before_purge(*filters, &blk)
37
- ActiveSupport::Deprecation.warn("before_purge will be removed without replacement in ActiveEncode 0.3")
38
- set_callback(:purge, :before, *filters, &blk)
39
- end
40
-
41
- def self.after_purge(*filters, &blk)
42
- ActiveSupport::Deprecation.warn("after_purge will be removed without replacement in ActiveEncode 0.3")
43
- set_callback(:purge, :after, *filters, &blk)
44
- end
45
-
46
- def self.around_purge(*filters, &blk)
47
- ActiveSupport::Deprecation.warn("around_purge will be removed without replacement in ActiveEncode 0.3")
48
- set_callback(:purge, :around, *filters, &blk)
49
- end
30
+ define_model_callbacks :create, :cancel
50
31
  end
51
32
  end
52
33
  end
@@ -22,9 +22,6 @@ module ActiveEncode
22
22
 
23
23
  attr_accessor :current_operations
24
24
  attr_accessor :percent_complete
25
-
26
- # @deprecated
27
- attr_accessor :tech_metadata
28
25
  end
29
26
 
30
27
  module ClassMethods
@@ -44,20 +41,14 @@ module ActiveEncode
44
41
  encode.send(:merge!, engine_adapter.find(id))
45
42
  end
46
43
  end
47
-
48
- def list(*args)
49
- ActiveSupport::Deprecation.warn("#list will be removed without replacement in ActiveEncode 0.3")
50
- engine_adapter.list(args)
51
- end
52
44
  end
53
45
 
54
46
  def initialize(input_url, options = nil)
55
47
  @input = Input.new.tap{ |input| input.url = input_url }
56
- @options = options || self.class.default_options(input_url)
48
+ @options = self.class.default_options(input_url).merge(Hash(options))
57
49
  end
58
50
 
59
51
  def create!
60
- # TODO: Raise ArgumentError if self has an id?
61
52
  run_callbacks :create do
62
53
  merge!(self.class.engine_adapter.create(self.input.url, self.options))
63
54
  end
@@ -69,18 +60,6 @@ module ActiveEncode
69
60
  end
70
61
  end
71
62
 
72
- def purge!
73
- ActiveSupport::Deprecation.warn("#purge! will be removed without replacement in ActiveEncode 0.3")
74
- run_callbacks :purge do
75
- self.class.engine_adapter.purge self
76
- end
77
- end
78
-
79
- def remove_output!(output_id)
80
- ActiveSupport::Deprecation.warn("#remove_output will be removed without replacement in ActiveEncode 0.3")
81
- self.class.engine_adapter.remove_output self, output_id
82
- end
83
-
84
63
  def reload
85
64
  run_callbacks :reload do
86
65
  merge!(self.class.engine_adapter.find(id))
@@ -91,15 +70,6 @@ module ActiveEncode
91
70
  !id.nil?
92
71
  end
93
72
 
94
- # @deprecated
95
- def tech_metadata
96
- metadata = {}
97
- [:width, :height, :frame_rate, :duration, :file_size,
98
- :audio_codec, :video_codec, :audio_bitrate, :video_bitrate, :checksum].each do |key|
99
- metadata[key] = input.send(key)
100
- end
101
- end
102
-
103
73
  protected
104
74
 
105
75
  def merge!(encode)
@@ -114,10 +84,6 @@ module ActiveEncode
114
84
  @current_operations = encode.current_operations
115
85
  @percent_complete = encode.percent_complete
116
86
 
117
- # deprecated
118
- @tech_metadata = encode.tech_metadata
119
- @finished_at = encode.finished_at
120
-
121
87
  self
122
88
  end
123
89
  end
@@ -8,13 +8,11 @@ module ActiveEncode
8
8
  module EngineAdapters
9
9
  extend ActiveSupport::Autoload
10
10
 
11
- autoload :ActiveJobAdapter
12
11
  autoload :MatterhornAdapter
13
- autoload :InlineAdapter
14
12
  autoload :ZencoderAdapter
15
- autoload :ShingoncoderAdapter
16
13
  autoload :ElasticTranscoderAdapter
17
14
  autoload :TestAdapter
15
+ autoload :FfmpegAdapter
18
16
 
19
17
  ADAPTER = 'Adapter'.freeze
20
18
  private_constant :ADAPTER
@@ -18,25 +18,12 @@ module ActiveEncode
18
18
  build_encode(get_job_details(id))
19
19
  end
20
20
 
21
- # TODO: implement list_jobs_by_pipeline and list_jobs_by_status
22
- def list(*_filters)
23
- raise NotImplementedError
24
- end
25
-
26
21
  # Can only cancel jobs with status = "Submitted"
27
22
  def cancel(id)
28
23
  response = client.cancel_job(id: id)
29
24
  build_encode(get_job_details(id)) if response.successful?
30
25
  end
31
26
 
32
- def purge(_encode)
33
- raise NotImplementedError
34
- end
35
-
36
- def remove_output(_encode, _output_id)
37
- raise NotImplementedError
38
- end
39
-
40
27
  private
41
28
 
42
29
  # Needs region and credentials setup per http://docs.aws.amazon.com/sdkforruby/api/Aws/ElasticTranscoder/Client.html
@@ -56,11 +43,20 @@ module ActiveEncode
56
43
  encode.current_operations = convert_current_operations(job)
57
44
  encode.percent_complete = convert_percent_complete(job)
58
45
  encode.created_at = convert_time(job.timing["submit_time_millis"])
59
- encode.updated_at = convert_time(job.timing["start_time_millis"])
60
- encode.finished_at = convert_time(job.timing["finish_time_millis"])
46
+ encode.updated_at = convert_time(job.timing["finish_time_millis"] || job.timing["start_time_millis"]) || encode.created_at
61
47
  encode.output = convert_output(job)
62
48
  encode.errors = convert_errors(job)
63
- encode.tech_metadata = convert_tech_metadata(job.input.detected_properties)
49
+
50
+ encode.input.id = job.input.key
51
+ tech_md = convert_tech_metadata(job.input.detected_properties)
52
+ [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
53
+ :audio_bitrate, :video_bitrate, :file_size].each do |field|
54
+ encode.input.send("#{field}=", tech_md[field])
55
+ end
56
+ encode.input.state = encode.state
57
+ encode.input.created_at = encode.created_at
58
+ encode.input.updated_at = encode.updated_at
59
+
64
60
  encode
65
61
  end
66
62
 
@@ -109,17 +105,27 @@ module ActiveEncode
109
105
  end
110
106
 
111
107
  def convert_output(job)
112
- output = []
113
- job.outputs.each do |o|
108
+ job.outputs.collect do |o|
114
109
  # It is assumed that the first part of the output key can be used to label the output
115
110
  # e.g. "quality-medium/somepath/filename.flv"
116
- label = o.key.split("/", 2).first
117
- url = job.output_key_prefix + o.key
118
- extras = { id: o.id, url: url, label: label }
119
- extras[:hls_url] = url + ".m3u8" if url.include?("/hls/") # TODO: find a better way to signal hls
120
- output << convert_tech_metadata(o).merge(extras)
111
+ output = ActiveEncode::Output.new
112
+ output.id = o.id
113
+ output.label = o.key.split("/", 2).first
114
+ output.url = job.output_key_prefix + o.key
115
+ # TODO: If HLS is considered distinct from this output then it should be a different output
116
+ # TODO: If HLS is not considered distinct from this output then this should be handled by a method on a ActiveEncode::Base subclass or consuming client
117
+ # extras[:hls_url] = url + ".m3u8" if url.include?("/hls/") # TODO: find a better way to signal hls
118
+ tech_md = convert_tech_metadata(o)
119
+ [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
120
+ :audio_bitrate, :video_bitrate, :file_size].each do |field|
121
+ output.send("#{field}=", tech_md[field])
122
+ end
123
+ output.state = convert_state(o)
124
+ output.created_at = convert_time(job.timing["submit_time_millis"])
125
+ output.updated_at = convert_time(job.timing["finish_time_millis"] || job.timing["start_time_millis"]) || output.created_at
126
+
127
+ output
121
128
  end
122
- output
123
129
  end
124
130
 
125
131
  def convert_errors(job)
@@ -130,9 +136,8 @@ module ActiveEncode
130
136
  return {} if props.blank?
131
137
  metadata_fields = {
132
138
  file_size: { key: :file_size, method: :itself },
133
- duration_millis: { key: :duration, method: :to_s },
134
- frame_rate: { key: :video_framerate, method: :itself },
135
- segment_duration: { key: :segment_duration, method: :itself },
139
+ duration_millis: { key: :duration, method: :to_i },
140
+ frame_rate: { key: :frame_rate, method: :to_i },
136
141
  width: { key: :width, method: :itself },
137
142
  height: { key: :height, method: :itself }
138
143
  }
@@ -144,7 +149,6 @@ module ActiveEncode
144
149
  next if conversion.nil?
145
150
  metadata[conversion[:key]] = value.send(conversion[:method])
146
151
  end
147
-
148
152
  metadata
149
153
  end
150
154
  end
@@ -0,0 +1,243 @@
1
+ require 'fileutils'
2
+ require 'nokogiri'
3
+
4
+ module ActiveEncode
5
+ module EngineAdapters
6
+ class FfmpegAdapter
7
+ WORK_DIR = ENV["ENCODE_WORK_DIR"] || "encodes" # Should read from config
8
+
9
+ def create(input_url, options = {})
10
+ new_encode = ActiveEncode::Base.new(input_url, options)
11
+ new_encode.id = SecureRandom.uuid
12
+ new_encode.created_at = Time.new
13
+ new_encode.updated_at = Time.new
14
+ new_encode.current_operations = []
15
+ new_encode.output = []
16
+
17
+ # Create a working directory that holds all output files related to the encode
18
+ FileUtils.mkdir_p working_path("", new_encode.id)
19
+ FileUtils.mkdir_p working_path("outputs", new_encode.id)
20
+
21
+ # Extract technical metadata from input file
22
+ `mediainfo --Output=XML --LogFile=#{working_path("input_metadata", new_encode.id)} #{input_url}`
23
+ new_encode.input = build_input new_encode
24
+
25
+ if new_encode.input.duration.blank?
26
+ new_encode.state = :failed
27
+ new_encode.percent_complete = 1
28
+
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
34
+
35
+ write_errors new_encode
36
+ return new_encode
37
+ end
38
+
39
+ new_encode.state = :running
40
+ new_encode.percent_complete = 1
41
+ new_encode.errors = []
42
+
43
+ # Run the ffmpeg command and save its pid
44
+ command = ffmpeg_command(input_url, new_encode.id, options)
45
+ pid = Process.spawn(command)
46
+ File.open(working_path("pid", new_encode.id), 'w') { |file| file.write pid }
47
+ new_encode.input.id = pid
48
+
49
+ # Prevent zombie process
50
+ Process.detach(pid)
51
+
52
+ new_encode
53
+ end
54
+
55
+ # Return encode object from file system
56
+ def find(id, opts={})
57
+ encode_class = opts[:cast]
58
+ encode = ActiveEncode::Base.new(nil, opts)
59
+ encode.id = id
60
+ encode.output = []
61
+ encode.created_at, encode.updated_at = get_times encode.id
62
+ encode.input = build_input encode
63
+ encode.percent_complete = calculate_percent_complete encode
64
+
65
+ pid = get_pid(id)
66
+ encode.input.id = pid if pid.present?
67
+
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
+ encode.current_operations = []
81
+ encode.created_at, encode.updated_at = get_times encode.id
82
+
83
+ if running? pid
84
+ encode.state = :running
85
+ encode.current_operations = ["transcoding"]
86
+ elsif progress_ended?(encode.id) && encode.percent_complete == 100
87
+ encode.state = :completed
88
+ elsif encode.percent_complete < 100
89
+ encode.state = :cancelled
90
+ end
91
+
92
+ encode.output = build_outputs encode if encode.completed?
93
+
94
+ encode
95
+ end
96
+
97
+ # Cancel ongoing encode using pid file
98
+ def cancel(id)
99
+ pid = get_pid(id)
100
+ Process.kill 'SIGTERM', pid.to_i
101
+
102
+ find id
103
+ end
104
+
105
+ private
106
+
107
+ def get_times id
108
+ 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
117
+ end
118
+
119
+ def write_errors encode
120
+ File.write(working_path("error.log", encode.id), encode.errors.join("\n"))
121
+ end
122
+
123
+ def build_input encode
124
+ input = ActiveEncode::Input.new
125
+ metadata = get_tech_metadata(working_path("input_metadata", encode.id))
126
+ input.url = metadata[:url]
127
+ input.assign_tech_metadata(metadata)
128
+ input.created_at = encode.created_at
129
+ input.updated_at = encode.created_at
130
+ input.id = "N/A"
131
+
132
+ input
133
+ end
134
+
135
+ def build_outputs encode
136
+ id = encode.id
137
+ outputs = []
138
+ Dir["#{File.absolute_path(working_path('outputs', id))}/*"].each do |file_path|
139
+ output = ActiveEncode::Output.new
140
+ 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]
144
+ output.id = "#{encode.input.id}-#{output.label}"
145
+ output.created_at = encode.created_at
146
+ output.updated_at = File.mtime file_path
147
+
148
+ # Extract technical metadata from output file
149
+ metadata_path = working_path("output_metadata-#{output.label}", id)
150
+ `mediainfo --Output=XML --LogFile=#{metadata_path} #{output.url}` unless File.file? metadata_path
151
+ output.assign_tech_metadata(get_tech_metadata(metadata_path))
152
+
153
+ outputs << output
154
+ end
155
+
156
+ outputs
157
+ end
158
+
159
+ def ffmpeg_command(input_url, id, opts)
160
+ output_opt = opts[:outputs].collect do |output|
161
+ file_name = "outputs/#{File.basename(input_url, File.extname(input_url))}-#{output[:label]}.#{output[:extension]}"
162
+ " #{output[:ffmpeg_opt]} #{working_path(file_name, id)}"
163
+ 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"
166
+ end
167
+
168
+ def get_pid(id)
169
+ if File.file? working_path("pid", id)
170
+ File.read(working_path("pid", id)).remove("\n")
171
+ else
172
+ nil
173
+ end
174
+ end
175
+
176
+ def working_path(path, id)
177
+ File.join(WORK_DIR, id, path)
178
+ end
179
+
180
+ def running?(pid)
181
+ begin
182
+ Process.getpgid pid.to_i
183
+ true
184
+ rescue Errno::ESRCH
185
+ false
186
+ end
187
+ end
188
+
189
+ def calculate_percent_complete encode
190
+ data = read_progress encode.id
191
+ if data.blank?
192
+ 1
193
+ else
194
+ (progress_value("out_time_ms=", data).to_i * 0.0001 / encode.input.duration).round
195
+ end
196
+ end
197
+
198
+ def read_progress id
199
+ if File.file? working_path("progress", id)
200
+ File.read working_path("progress", id)
201
+ else
202
+ nil
203
+ end
204
+ end
205
+
206
+ def progress_ended? id
207
+ "end" == progress_value("progress=", read_progress(id))
208
+ end
209
+
210
+ def progress_value key, data
211
+ if data.present? && key.present?
212
+ ri = data.rindex(key) + key.length
213
+ data[ri..data.index("\n", ri)-1]
214
+ else
215
+ nil
216
+ end
217
+ end
218
+
219
+ def get_tech_metadata file_path
220
+ doc = Nokogiri::XML File.read(file_path)
221
+ doc.remove_namespaces!
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: get_xpath_text(doc, '//Duration/text()', :to_f),
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
+ if doc.xpath(xpath).first
236
+ doc.xpath(xpath).first.text.send(cast_method)
237
+ else
238
+ nil
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end