enigmamachine 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.document +5 -0
  2. data/.gitignore +26 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +32 -0
  5. data/Rakefile +57 -0
  6. data/VERSION +1 -0
  7. data/bin/enigmamachine +15 -0
  8. data/lib/enigmamachine/config.ru +9 -0
  9. data/lib/enigmamachine/encoding_queue.rb +29 -0
  10. data/lib/enigmamachine/models/encoder.rb +54 -0
  11. data/lib/enigmamachine/models/encoding_task.rb +19 -0
  12. data/lib/enigmamachine/models/video.rb +37 -0
  13. data/lib/enigmamachine/public/default.css +233 -0
  14. data/lib/enigmamachine/public/images/Enigma-logo.jpg +0 -0
  15. data/lib/enigmamachine/public/images/bg01.jpg +0 -0
  16. data/lib/enigmamachine/public/images/bg02.jpg +0 -0
  17. data/lib/enigmamachine/public/images/bg03.jpg +0 -0
  18. data/lib/enigmamachine/public/images/bg04.jpg +0 -0
  19. data/lib/enigmamachine/public/images/img02.gif +0 -0
  20. data/lib/enigmamachine/public/images/img03.gif +0 -0
  21. data/lib/enigmamachine/public/images/img04.gif +0 -0
  22. data/lib/enigmamachine/public/images/img05.gif +0 -0
  23. data/lib/enigmamachine/public/images/img06.jpg +0 -0
  24. data/lib/enigmamachine/public/images/spacer.gif +0 -0
  25. data/lib/enigmamachine/views/encoders/edit.erb +20 -0
  26. data/lib/enigmamachine/views/encoders/encoder.erb +10 -0
  27. data/lib/enigmamachine/views/encoders/encoding_task.erb +10 -0
  28. data/lib/enigmamachine/views/encoders/form.erb +5 -0
  29. data/lib/enigmamachine/views/encoders/index.erb +12 -0
  30. data/lib/enigmamachine/views/encoders/new.erb +19 -0
  31. data/lib/enigmamachine/views/encoders/show.erb +19 -0
  32. data/lib/enigmamachine/views/encoding_tasks/edit.erb +23 -0
  33. data/lib/enigmamachine/views/encoding_tasks/form.erb +12 -0
  34. data/lib/enigmamachine/views/encoding_tasks/new.erb +23 -0
  35. data/lib/enigmamachine/views/index.erb +13 -0
  36. data/lib/enigmamachine/views/layout.erb +67 -0
  37. data/lib/enigmamachine/views/videos/form.erb +11 -0
  38. data/lib/enigmamachine/views/videos/index.erb +36 -0
  39. data/lib/enigmamachine/views/videos/new.erb +19 -0
  40. data/lib/enigmamachine/views/videos/video.erb +6 -0
  41. data/lib/enigmamachine.rb +179 -0
  42. data/lib/enigmamachine.sqlite3 +0 -0
  43. data/lib/ext/array_ext.rb +8 -0
  44. data/lib/ext/partials.rb +28 -0
  45. data/lib/init.rb +75 -0
  46. data/test/helper.rb +49 -0
  47. data/test/support/afile.mpg +0 -0
  48. data/test/support/blueprints.rb +29 -0
  49. data/test/test_encoder.rb +80 -0
  50. data/test/test_encoder_queue.rb +34 -0
  51. data/test/test_enigmamachine.rb +536 -0
  52. data/test/test_video.rb +84 -0
  53. metadata +141 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,26 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ *.log
23
+ *.sqlite3
24
+ *.mpg
25
+ *.flv
26
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 dave
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,32 @@
1
+ = enigmamachine
2
+
3
+ Enigmamachine is a video processor which queues and encodes videos according
4
+ to target profiles that you define. Videos must be on a locally mounted
5
+ filesystem. The processor takes the path to the video, and executes
6
+ multiple ffmpeg commands on the video. There is a handy web interface for
7
+ defining encoding tasks, and a restful web service which takes encoding commands.
8
+
9
+ Enigmamachine is written using Sinatra, Thin, and Eventmachine.
10
+
11
+ == Current Status
12
+
13
+ The basic Sinatra application and the encoding queue work, but the app needs to
14
+ be packaged up as a gem and all the niceties of running the server need to be
15
+ made more convenient.
16
+
17
+ The biggest problem is that it was done on a whim (i.e. without test-first
18
+ development) since it was just a sketch at first - this needs to be fixed.
19
+
20
+ == Note on Patches/Pull Requests
21
+
22
+ * Fork the project.
23
+ * Make your feature addition or bug fix.
24
+ * Add tests for it. This is important so I don't break it in a
25
+ future version unintentionally.
26
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
27
+ * Send me a pull request. Bonus points for topic branches.
28
+
29
+ == Copyright
30
+
31
+ Copyright (c) 2010 Dave Hrycyszyn. See LICENSE for details.
32
+
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "enigmamachine"
8
+ gem.summary = %Q{A RESTful video encoder.}
9
+ gem.description = %Q{A RESTful video encoder which you can use as either a front-end to ffmpeg or headless on a server.}
10
+ gem.email = "dave@caprica"
11
+ gem.homepage = "http://github.com/futurechimp/enigmamachine"
12
+ gem.authors = ["dave"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_dependency "dm-core", "=0.10.2"
15
+ gem.add_dependency "eventmachine", "=0.12.10"
16
+
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "enigmamachine #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
57
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/enigmamachine ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Enigmamachine command line interface script.
4
+ # Run <tt>enigmamachine -h</tt> to get more usage.
5
+ require File.dirname(__FILE__) + '/../lib/enigmamachine'
6
+ require 'thin'
7
+
8
+ rackup_file = "#{File.dirname(__FILE__)}/../lib/enigmamachine/config.ru"
9
+
10
+ argv = ARGV
11
+ argv << ["-R", rackup_file] unless ARGV.include?("-R")
12
+ argv << ["-p", "2002"] unless ARGV.include?("-p")
13
+ argv << ["-e", "production"] unless ARGV.include?("-e")
14
+ Thin::Runner.new(argv.flatten).run!
15
+
@@ -0,0 +1,9 @@
1
+ require 'sinatra'
2
+
3
+ set :run => false
4
+ set :environment => :production
5
+
6
+ disable :reload
7
+
8
+ run Sinatra::Application
9
+
@@ -0,0 +1,29 @@
1
+ class EncodingQueue
2
+
3
+ # Adds a periodic timer to the Eventmachine reactor loop and immediately
4
+ # starts looking for unencoded videos.
5
+ #
6
+ def start
7
+ EM.add_periodic_timer(5) {
8
+ encode_next_video
9
+ }
10
+ encode_next_video
11
+ end
12
+
13
+
14
+ # Gets the next unencoded Video from the database and starts encoding it.
15
+ #
16
+ def encode_next_video
17
+ if Video.unencoded.count > 0 && ::Video.encoding.count == 0
18
+ video = Video.unencoded.first
19
+ Log.info("Starting to encode video: #{video.id}")
20
+ begin
21
+ video.encoder.encode(video)
22
+ rescue Exception => ex
23
+ Log.error("Video #{video.id} failed to encode due to error: #{ex}")
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,54 @@
1
+ require File.dirname(__FILE__) + '/video'
2
+
3
+ # An encoding profile which can be applied to a video. It has a name and is
4
+ # composed of a bunch of EncodingTasks.
5
+ #
6
+ class Encoder
7
+ include DataMapper::Resource
8
+
9
+ # Properties
10
+ #
11
+ property :id, Serial
12
+ property :name, String, :required => true, :length => (1..254)
13
+
14
+ # Associations
15
+ #
16
+ has n, :encoding_tasks
17
+
18
+ # This is a candidate for being moved into the class Encoders::Video (which
19
+ # could happily be renamed as something like Encoders::Ffmpeg).
20
+ #
21
+ def encode(video)
22
+ ffmpeg(encoding_tasks.first, video)
23
+ end
24
+
25
+ private
26
+
27
+ # If the encode method is pulled into another class, this one should go with
28
+ # it, obviously.
29
+ #
30
+ def ffmpeg(task, video)
31
+ current_task_index = encoding_tasks.index(task)
32
+ command_string = "ffmpeg -i #{video.file} #{task.command} #{video.file + task.output_file_suffix} -y"
33
+ encoding_operation = proc {
34
+ video.state = "encoding"
35
+ video.save
36
+ Log.info("Executing: #{task.name} on video #{video.id}")
37
+ `nice -n 19 #{command_string}`
38
+ }
39
+ completion_callback = proc {|result|
40
+ if task == encoding_tasks.last
41
+ video.state = "complete"
42
+ video.save
43
+ Log.info("Video #{video.id} finished")
44
+ else
45
+ next_task_index = current_task_index + 1
46
+ next_task = encoding_tasks[next_task_index]
47
+ ffmpeg(next_task, video)
48
+ end
49
+ }
50
+ EventMachine.defer(encoding_operation, completion_callback)
51
+ end
52
+
53
+ end
54
+
@@ -0,0 +1,19 @@
1
+ # A task which defines how a video will be encoded.
2
+ #
3
+ class EncodingTask
4
+ include DataMapper::Resource
5
+
6
+ # Properties
7
+ #
8
+ property :id, Serial
9
+ property :name, String, :required => true, :length => (1..254)
10
+ property :output_file_suffix, String, :required => true, :length => (1..254)
11
+ property :command, String, :required => true, :length => (1..254)
12
+ property :encoder_id, Integer
13
+
14
+ # Associations
15
+ #
16
+ belongs_to :encoder
17
+
18
+ end
19
+
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/encoder'
2
+
3
+ # A video which we want to encode.
4
+ #
5
+ class Video
6
+ include DataMapper::Resource
7
+
8
+ # Properties
9
+ #
10
+ property :id, Serial
11
+ property :file, String, :required => true, :length => (1..254)
12
+ property :state, String, :required => true,
13
+ :length => (1..10), :default => 'unencoded'
14
+ property :created_at, DateTime
15
+ property :updated_at, DateTime
16
+ property :encoder_id, Integer, :required => true
17
+
18
+ belongs_to :encoder
19
+
20
+ def self.unencoded
21
+ all(:state => 'unencoded')
22
+ end
23
+
24
+ def self.encoding
25
+ all(:state => 'encoding')
26
+ end
27
+
28
+ def self.with_errors
29
+ all(:state => 'error')
30
+ end
31
+
32
+ def self.complete
33
+ all(:state => 'complete', :order => [:updated_at.desc])
34
+ end
35
+
36
+ end
37
+
@@ -0,0 +1,233 @@
1
+ /*
2
+ Design by Free CSS Templates
3
+ http://www.freecsstemplates.org
4
+ Released for free under a Creative Commons Attribution 2.5 License
5
+ */
6
+
7
+ body {
8
+ margin: 0;
9
+ padding: 0;
10
+ background: #FFFFFF url(images/bg01.jpg) repeat-x top left;
11
+ font-size: 13px;
12
+ color: #7F7F7F;
13
+ }
14
+
15
+ body, th, td, input, textarea, select, option {
16
+ font-weight: normal;
17
+ font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
18
+ }
19
+
20
+ h1, h2, h3 {
21
+ font-weight: normal;
22
+ color: #484848;
23
+ }
24
+
25
+ h1 {
26
+ text-transform: lowercase;
27
+ letter-spacing: -1px;
28
+ font-size: 24px;
29
+ }
30
+
31
+ h2 {
32
+ text-transform: lowercase;
33
+ letter-spacing: -1px;
34
+ font-size: 24px;
35
+ }
36
+
37
+ h3 {
38
+ font-size: 1em;
39
+ }
40
+
41
+ p, ul, ol {
42
+ line-height: 200%;
43
+ }
44
+
45
+ blockquote {
46
+ padding-left: 1em;
47
+ }
48
+
49
+ blockquote p, blockquote ul, blockquote ol {
50
+ line-height: normal;
51
+ font-style: italic;
52
+ }
53
+
54
+ a {
55
+ color: #7ACE11;
56
+ }
57
+
58
+ a:hover {
59
+ text-decoration: none;
60
+ color: #7ACE11;
61
+ }
62
+
63
+ /* Header */
64
+
65
+ #header {
66
+ width: 890px;
67
+ height: 190px;
68
+ margin: 0px auto;
69
+ }
70
+
71
+ /* Logo */
72
+
73
+ #logo {
74
+ float: left;
75
+ padding: 40px 0 0 0;
76
+ }
77
+
78
+ #logo h1 {
79
+ margin: 0;
80
+ text-transform: lowercase;
81
+ letter-spacing: -2px;
82
+ font-size: 3.6em;
83
+ font-weight: normal;
84
+ color: #FFFFFF;
85
+ }
86
+
87
+ #logo h1 a {
88
+ padding-right: 20px;
89
+ text-decoration: none;
90
+ color: #FFFFFF;
91
+ }
92
+
93
+ #logo p {
94
+ margin: -5px 0 0 0;
95
+ text-transform: uppercase;
96
+ font-size: 1.22em;
97
+ letter-spacing: -1px;
98
+ }
99
+
100
+ #logo a {
101
+ text-decoration: none;
102
+ color: #FFFFFF;
103
+ }
104
+
105
+ /* Menu */
106
+
107
+ #menu {
108
+ float: right;
109
+ }
110
+
111
+ #menu ul {
112
+ margin: 0px;
113
+ padding: 93px 0px 0px 0px;
114
+ list-style: none;
115
+ }
116
+
117
+ #menu li {
118
+ display: inline;
119
+ }
120
+
121
+ #menu a {
122
+ display: block;
123
+ float: left;
124
+ margin-left: 20px;
125
+ text-decoration: none;
126
+ text-transform: lowercase;
127
+ font-size: 1.36em;
128
+ color: #FFFFFF;
129
+ }
130
+
131
+ #menu a:hover, .active a {
132
+ border-bottom: 3px solid #FFFFFF;
133
+ }
134
+
135
+
136
+ /* Page */
137
+
138
+ #page {
139
+ width: 890px;
140
+ margin: 0 auto;
141
+ }
142
+
143
+ /* Content */
144
+
145
+ #content {
146
+ float: left;
147
+ width: 590px;
148
+ }
149
+
150
+ .post {
151
+ padding: 20px 20px;
152
+ background: url(images/bg04.jpg) no-repeat top left;
153
+ }
154
+
155
+ .title {
156
+ margin: 0;
157
+ border-bottom: 2px solid #484848;
158
+ color: #484848;
159
+ }
160
+
161
+ .byline {
162
+ margin: 0;
163
+ color: #D79B00;
164
+ }
165
+
166
+ .meta {
167
+ text-align: left;
168
+ color: #646464;
169
+ }
170
+
171
+ .meta .more {
172
+ padding-left: 20px;
173
+ background: url(images/img03.gif) no-repeat left center;
174
+ }
175
+
176
+ .meta .comments {
177
+ padding-left: 20px;
178
+ background: url(images/img04.gif) no-repeat left center;
179
+ }
180
+
181
+ /* Sidebar */
182
+
183
+ #sidebar {
184
+ float: right;
185
+ width: 240px;
186
+ }
187
+
188
+ #sidebar ul {
189
+ margin: 0;
190
+ padding: 0;
191
+ list-style: none;
192
+ }
193
+
194
+ #sidebar li {
195
+ }
196
+
197
+ #sidebar li ul {
198
+ padding: 15px 0;
199
+ }
200
+
201
+ #sidebar li li {
202
+ padding-left: 20px;
203
+ border-bottom: 1px dotted #7B9418;
204
+ }
205
+
206
+ #sidebar h2 {
207
+ margin: 0;
208
+ padding: 5px 0 0 20px;
209
+ background: url(images/img06.jpg) no-repeat left 80%;
210
+ }
211
+
212
+ #sidebar a {
213
+ text-decoration: none;
214
+ }
215
+
216
+ #sidebar a:hover {
217
+ }
218
+
219
+ /* Footer */
220
+
221
+ #footer {
222
+ clear: both;
223
+ margin: 0px;
224
+ height: 80px;
225
+ background: #F2F2F2 url(images/bg02.jpg) repeat-x left top;
226
+ }
227
+
228
+ #footer p {
229
+ padding: 20px 0;
230
+ text-align: center;
231
+ font-size: smaller;
232
+ color: #717171;
233
+ }
@@ -0,0 +1,20 @@
1
+ <div class="post">
2
+ <h1 class="title">Edit resource</h1>
3
+ <div class="entry">
4
+ <% unless @encoder.errors.empty? %>
5
+ <div class="errors">
6
+ <h2>Errors on Encoder</h2>
7
+ <ul>
8
+ <% @encoder.errors.each do |error| %>
9
+ <li><%= error %></li>
10
+ <% end %>
11
+ </ul>
12
+ </div>
13
+ <% end %>
14
+
15
+ <form action="/encoders/<%= @encoder.id%>" method="POST">
16
+ <input name="_method" value="PUT" type="hidden"/>
17
+ <%= partial :"encoders/form" %>
18
+ </form>
19
+ </div>
20
+
@@ -0,0 +1,10 @@
1
+ <li>
2
+ <%= encoders_encoder.name %> -
3
+ <a href="/encoders/<%= encoders_encoder.id %>">show</a>
4
+ <a href="/encoders/<%= encoders_encoder.id %>/edit">edit</a>
5
+ <form action="/encoders/<%= encoders_encoder.id %>" method="POST">
6
+ <input name="_method" value="DELETE" type="hidden"/>
7
+ <input type="submit" value="Delete"/>
8
+ </form>
9
+ </li>
10
+
@@ -0,0 +1,10 @@
1
+ <% task = encoders_encoding_task %>
2
+ <div id="task<%= task.id %>">
3
+ <strong><%= task.name %>:</strong> ffmpeg <%= task.command %> :: <i><%= task.output_file_suffix %></i> suffix :: <a href="/encoding_tasks/<%= task.id%>/edit">edit</a>
4
+
5
+ <form action="/encoding_tasks/<%= task.id %>" method="POST">
6
+ <input name="_method" value="DELETE" type="hidden"/>
7
+ <input type="submit" value="Delete"/>
8
+ </form>
9
+ </div>
10
+
@@ -0,0 +1,5 @@
1
+ <p><strong>Name:</strong>
2
+ <input name="encoder[name]" id="encoder_name" value="<%= @encoder.name %>" size="50"></input></p>
3
+ <input type="submit" value="Save"/>
4
+ <p><a href="/encoders">cancel</a></p>
5
+
@@ -0,0 +1,12 @@
1
+ <div class="post">
2
+ <h1 class="title">Available encoders</h1>
3
+ <div class="entry">
4
+ <% unless @encoders.empty? %>
5
+ <ul>
6
+ <%= partial :'encoders/encoder', :collection => @encoders %>
7
+ </ul>
8
+ <% end %>
9
+ <p><a href="/encoders/new">New encoder</a></p>
10
+ </div>
11
+ </div>
12
+
@@ -0,0 +1,19 @@
1
+ <div class="post">
2
+ <h1 class="title">Create a new encoder</h1>
3
+ <div class="entry">
4
+ <% unless @encoder.errors.empty? %>
5
+ <div class="errors">
6
+ <h2>Errors on Encoder</h2>
7
+ <ul>
8
+ <% @encoder.errors.each do |error| %>
9
+ <li><%= error %></li>
10
+ <% end %>
11
+ </ul>
12
+ </div>
13
+ <% end %>
14
+
15
+ <form action="/encoders" method="POST">
16
+ <%= partial :"encoders/form" %>
17
+ </form>
18
+ </div>
19
+
@@ -0,0 +1,19 @@
1
+ <div class="post">
2
+ <h1 class="title">Encoder: <%= @encoder.name %></h1>
3
+ <div class="entry">
4
+
5
+
6
+
7
+ <p>Encoding tasks that this encoder performs:</p>
8
+ <% if @encoder.encoding_tasks.empty? %>
9
+ None.
10
+ <% end %>
11
+ <%= partial(:"encoders/encoding_task", :collection => @encoder.encoding_tasks) %>
12
+
13
+ <p>Add a <a href="/encoding_tasks/new/<%= @encoder.id%>">new</a> task to this encoder.</p>
14
+
15
+ <p><a href="/encoders">Back</a> to the list of available encoders.</p>
16
+
17
+ </div>
18
+ </div>
19
+