active_encode 0.2 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +9 -0
- data/README.md +1 -16
- data/active_encode.gemspec +4 -4
- data/app/jobs/active_encode/polling_job.rb +4 -4
- data/lib/active_encode/callbacks.rb +2 -21
- data/lib/active_encode/core.rb +1 -35
- data/lib/active_encode/engine_adapters.rb +1 -3
- data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +32 -28
- data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +243 -0
- data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +64 -97
- data/lib/active_encode/engine_adapters/test_adapter.rb +0 -12
- data/lib/active_encode/engine_adapters/zencoder_adapter.rb +53 -44
- data/lib/active_encode/input.rb +6 -0
- data/lib/active_encode/output.rb +7 -0
- data/lib/active_encode/persistence.rb +1 -1
- data/lib/active_encode/polling.rb +2 -2
- data/lib/active_encode/status.rb +0 -3
- data/lib/active_encode/technical_metadata.rb +7 -0
- data/lib/active_encode/version.rb +1 -1
- data/spec/fixtures/ffmpeg/cancelled-id/error.log +0 -0
- data/spec/fixtures/ffmpeg/cancelled-id/input_metadata +90 -0
- data/spec/fixtures/ffmpeg/cancelled-id/pid +1 -0
- data/spec/fixtures/ffmpeg/cancelled-id/progress +11 -0
- data/spec/fixtures/ffmpeg/completed-id/error.log +0 -0
- data/spec/fixtures/ffmpeg/completed-id/input_metadata +102 -0
- data/spec/fixtures/ffmpeg/completed-id/output_metadata-high +90 -0
- data/spec/fixtures/ffmpeg/completed-id/output_metadata-low +90 -0
- data/spec/fixtures/ffmpeg/completed-id/pid +1 -0
- data/spec/fixtures/ffmpeg/completed-id/progress +11 -0
- data/spec/fixtures/ffmpeg/completed-id/video-high.mp4 +0 -0
- data/spec/fixtures/ffmpeg/completed-id/video-low.mp4 +0 -0
- data/spec/fixtures/ffmpeg/failed-id/error.log +1 -0
- data/spec/fixtures/ffmpeg/failed-id/input_metadata +90 -0
- data/spec/fixtures/ffmpeg/failed-id/pid +1 -0
- data/spec/fixtures/ffmpeg/failed-id/progress +11 -0
- data/spec/fixtures/ffmpeg/running-id/error.log +0 -0
- data/spec/fixtures/ffmpeg/running-id/input_metadata +90 -0
- data/spec/fixtures/ffmpeg/running-id/pid +1 -0
- data/spec/fixtures/ffmpeg/running-id/progress +11 -0
- data/spec/fixtures/fireworks.mp4 +0 -0
- data/spec/integration/elastic_transcoder_adapter_spec.rb +21 -12
- data/spec/integration/ffmpeg_adapter_spec.rb +120 -0
- data/spec/integration/matterhorn_adapter_spec.rb +30 -59
- data/spec/integration/zencoder_adapter_spec.rb +242 -22
- data/spec/shared_specs/engine_adapter_specs.rb +116 -16
- data/spec/units/core_spec.rb +31 -0
- data/spec/units/input_spec.rb +25 -0
- data/spec/units/output_spec.rb +28 -1
- data/spec/units/polling_job_spec.rb +10 -10
- metadata +51 -11
- data/lib/active_encode/engine_adapters/active_job_adapter.rb +0 -21
- data/lib/active_encode/engine_adapters/inline_adapter.rb +0 -47
- data/lib/active_encode/engine_adapters/shingoncoder_adapter.rb +0 -63
- data/spec/integration/shingoncoder_adapter_spec.rb +0 -170
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e507e3110f65efc706aed54986fd98043fb0911
|
4
|
+
data.tar.gz: 0a5a69eabd90c026e23258156f2d4f93a136a4fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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 )
|
data/active_encode.gemspec
CHANGED
@@ -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 = "
|
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 :
|
8
|
-
encode.run_callbacks(:
|
7
|
+
when :failed
|
8
|
+
encode.run_callbacks(:failed) { encode }
|
9
9
|
when :cancelled
|
10
10
|
encode.run_callbacks(:cancelled) { encode }
|
11
|
-
when :
|
12
|
-
encode.run_callbacks(:
|
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
|
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
|
data/lib/active_encode/core.rb
CHANGED
@@ -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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
output
|
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: :
|
134
|
-
frame_rate: { key: :
|
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
|