video-sprites 0.0.1.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43859ff5512d89e5dbefb3e3eff88c9660af15a9
4
+ data.tar.gz: e8feed13b4c139db06e452795cf24619a729b52a
5
+ SHA512:
6
+ metadata.gz: 90166c4cc92205cedfe25d6a72bd10b4f96698db08c5eea9d2271b55cbbe6f46cb1ff03c3fb2c428f2faaee67ad3f6e9d48815575a1c07e01f158308da4946e2
7
+ data.tar.gz: 5d1a34832889aacd3720711e18884da2f980aaf4670e62b958169b7940984e69ee7c67bae068def6001a6449b6b529e85eaa690a90fac4bda4e9b1e3dcfbce4e
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .idea
16
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in video-sprites.gemspec
4
+ gemspec
5
+ 'slop'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jason Ronallo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,49 @@
1
+ # Video Sprites
2
+
3
+ Exports thumbnail images, thumbnail sprite image, and a WebVTT metadata file with synced media fragment URLs to thumbnails within the sprite.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem install 'video-sprites'
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - ffmpeg
14
+ - imagemagick (montage)
15
+
16
+ ## Usage
17
+
18
+ ```shell
19
+ video-sprites --help
20
+ ```
21
+
22
+ ```shell
23
+ video-sprites --seconds 5 --width 200 --columns 5 --input . --output ./output
24
+ ```
25
+
26
+ ## Test Media Sources
27
+
28
+ https://www.youtube.com/watch?v=dTCEDG9h9AA
29
+
30
+ https://www.youtube.com/watch?v=9AGisNPUBqM
31
+
32
+ https://www.youtube.com/watch?v=Z9To9NOLEPI
33
+
34
+ https://www.youtube.com/watch?v=Ww4WrcjAOlo
35
+
36
+ https://www.youtube.com/watch?v=wz-eInv9f7g
37
+
38
+ ## TODO
39
+
40
+ - Consider adding an option to change the output filename.
41
+ - Optionally allow for scene change detection and variable length cues. How difficult would this be?
42
+ - Should the first timestamp after the first cue not be on the second but be a fractional second instead?
43
+
44
+ ## Authors
45
+
46
+ - Ashley Blewer
47
+ - Jay Brown
48
+ - Jason Ronallo
49
+ - Nicholas Zoss
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'slop'
4
+ require 'fileutils'
5
+
6
+ # remove all JPEG and VTT files, but leave the directories in place.
7
+ def remove_files(output_directory, verbose)
8
+ deadfiles = Dir.glob(File.join(output_directory, '**', '*.jpg'))
9
+ puts "list of dead files will be #{deadfiles}" if verbose
10
+ deadfiles.each do | deadfile |
11
+ puts "removing file #{deadfile}" if verbose
12
+ File.unlink(deadfile)
13
+ end
14
+ deadfiles = Dir.glob(File.join(output_directory, '**', '*.vtt'))
15
+ puts "list of dead files will be #{deadfiles}" if verbose
16
+ deadfiles.each do | deadfile |
17
+ puts "removing file #{deadfile}" if verbose
18
+ File.unlink(deadfile)
19
+ end
20
+ end
21
+
22
+ # Format a WebVTT timestamp according to the specification using hours, minutes, seconds, and fractional seconds.
23
+ def timestamp(total_seconds)
24
+ seconds = total_seconds % 60
25
+ minutes = (total_seconds / 60) % 60
26
+ hours = total_seconds / (60 * 60)
27
+
28
+ format("%02d:%02d:%02d.000", hours, minutes, seconds)
29
+ end
30
+
31
+ opts = Slop.new strict: true do
32
+ banner 'Usage: video-sprites [options]'
33
+ on 'i', 'input=', 'Input file or directory', required: true
34
+ on 'o', 'output=', 'Output file or directory', argument: :optional
35
+ on 's', 'seconds=', 'Seconds interval between snapshots', argument: :optional, as: Integer, default: 5
36
+ on 'w', 'width=', 'Width of each thumbnail', argument: :optional, as: Integer, default: 200
37
+ on 'c', 'columns=', 'Number of columns in the image sprite', argument: :optional, as: Integer, default: 5
38
+ on 'r', 'rows=', 'Maximum number of rows to put into each sprite', argument: :optional, as: Integer
39
+ # on 'k', 'keep', 'Keep all the individual images and other intermediate artifacts.', argument: :optional
40
+ on 't', 'start', 'Start time to begin from in seconds or hh:mm:ss[.xxx]', argument: :optional
41
+ on 'd', 'duration', 'Duration of the clip to extract stills from in seconds or hh:mm:ss[.xxx] and can only be set if --start is also set.', argument: :optional
42
+ on 'u', 'url=', "Base url to use for webvtt", default: 'http://example.com'
43
+ on 'g', 'gif', "Create a GIF animation as well?"
44
+ on 'v', 'verbose', 'Enable verbose mode'
45
+ on 'z', 'clean', 'Clean up the output directory'
46
+ on 'h', 'help', 'Help!!!'
47
+ end
48
+
49
+ begin
50
+ opts.parse
51
+ rescue Slop::Error => e
52
+ puts e.message
53
+ puts opts # print help
54
+ exit
55
+ end
56
+
57
+ verbose = opts[:verbose]
58
+ input = File.expand_path(opts[:input])
59
+ output = opts[:output]
60
+ seconds = opts[:seconds]
61
+ columns = opts[:columns]
62
+ width = opts[:width]
63
+ clean = opts[:clean]
64
+
65
+ unless File.exist?(input)
66
+ puts 'File or directory does not exist!'
67
+ exit
68
+ end
69
+
70
+ if verbose
71
+ puts "Input file: #{input}"
72
+ end
73
+
74
+ if File.directory?(input)
75
+ # TODO: Only pull in video files with this. What's the best way to do that?
76
+ # Should we test for whether a file is a video file or use something potentially more
77
+ # fragile like testing for extensions?
78
+ files = Dir.glob(File.join(input, '*'))
79
+ else
80
+ files = [input]
81
+ end
82
+
83
+ # Since we allow for input to be either a file or a directory we need to set file paths
84
+ # for use later appropriately.
85
+ if output
86
+ output_directory = File.expand_path(output)
87
+ puts "output dir is #{output_directory}" if verbose
88
+ if File.directory?(output_directory)
89
+ remove_files(output_directory, verbose) if clean
90
+ elsif File.exist?(output_directory)
91
+ raise "Output directory must be a directory, was: #{output}"
92
+ else
93
+ FileUtils.mkdir_p(output_directory)
94
+ end
95
+ else
96
+ output_directory = input
97
+ if File.directory?(output_directory)
98
+ puts "output dir is #{output_directory}" if verbose
99
+ remove_files(output_directory, verbose) if clean
100
+ else
101
+ output_directory = File.dirname(input)
102
+ puts "output dir is #{output_directory}" if verbose
103
+ remove_files(output_directory, verbose) if clean
104
+ end
105
+ end
106
+
107
+ # This is the main loop where we go through video files.
108
+ files.each do | file |
109
+
110
+ # for repeated runs, skip any directories in this directory
111
+ if File.directory?(file)
112
+ next
113
+ end
114
+
115
+ puts "Processing file: #{file}" if verbose
116
+ extension = File.extname(file)
117
+ basename = File.basename(file, extension)
118
+ puts "Basename is: #{basename}" if verbose
119
+
120
+ output_path = File.join(output_directory, basename)
121
+
122
+ if File.directory?(input)
123
+ FileUtils.mkdir_p(output_path)
124
+ output_file_path = File.join(output_directory, basename, basename)
125
+ else
126
+ output_file_path = File.join(output_directory, basename)
127
+ end
128
+
129
+ puts "#{output_file_path} output file path"
130
+ # FIXME: check if the files exist already and maybe don't process anything
131
+
132
+ # Use ffprobe to discover the frames per second.
133
+ ffprobe_cmd = 'ffprobe -i "' + "#{file}" + '" 2>&1 | sed -n "s/.*, \(.*\) fp.*/\1/p"'
134
+ puts ffprobe_cmd if verbose
135
+ # Frames per second can be fractional so round up
136
+ frames_per_sec=`#{ffprobe_cmd}`.to_f.ceil.to_i
137
+ frames_value=frames_per_sec * seconds
138
+ puts "frames_per_sec #{frames_per_sec}, frames_value #{frames_value}" if verbose
139
+
140
+ # Using select allows for more exact selection of a frame at the times we want
141
+ ffmpeg_cmd = %Q|ffmpeg -i "#{file}" -vf select='lt(mod(n\\,#{frames_value})\\,1),fps=1/#{seconds}' |
142
+ if opts[:start]
143
+ ffmpeg_cmd += " -ss #{opts[:start]} "
144
+ if opts[:duration]
145
+ ffmpeg_cmd += " -t #{opts[:duration]} "
146
+ end
147
+ end
148
+ ffmpeg_cmd += %Q| "#{output_file_path}-%05d.jpg" |
149
+ puts ffmpeg_cmd if verbose
150
+ `#{ffmpeg_cmd}`
151
+
152
+ # We drop the first snapshot because it is the first frame which will either be useless or already in our poster
153
+ # image for the video. This also allows the WebVTT to show the thumbnail of a frame that will come some time after
154
+ # the time selected/clicked on.
155
+ jpegs = Dir.glob(output_file_path + '*.jpg').sort
156
+ FileUtils.rm(jpegs.first)
157
+ jpegs.shift
158
+
159
+ if opts[:gif]
160
+ `convert -delay 20 -loop 0 "#{output_file_path}*.jpg" #{output_file_path}.gif`
161
+ end
162
+
163
+ # Montage concatenates all the images into an image sprite.
164
+ # TODO: Instead of creating a single sprite take a slice of the output jpegs and create a number of different
165
+ # sprites.
166
+ slice_size = jpegs.length
167
+ if opts[:rows]
168
+ slice_size = opts[:columns] * opts[:rows]
169
+ end
170
+ jpegs.each_slice(slice_size).with_index do | jpeg_slice, index |
171
+ montage_files = jpeg_slice.map{|jpeg| %Q|"#{jpeg}"|}.join(" ")
172
+ padded_index = (index + 1).to_s.rjust(5, "0")
173
+ montage_cmd = %Q|montage #{montage_files} -tile #{columns}x -geometry #{width}x "#{output_file_path}-sprite-#{padded_index}.jpg"|
174
+ puts montage_cmd if verbose
175
+ `#{montage_cmd}`
176
+ end
177
+
178
+ # Prepare to create WebVTT file
179
+ if verbose
180
+ puts 'Processing a WebVTT file from these images:'
181
+ puts jpegs
182
+ end
183
+ # place all the cues into an array of hashes like this:
184
+ # {start: 0, end: 5, x: 0, y: 0, w: 200, h: 150}
185
+ cues = []
186
+ start = 0
187
+
188
+ # Use the first jpeg to determine the height of the resulting thumbs. We already know the width from the
189
+ # passed in option or the default width.
190
+ first_jpeg = jpegs.first
191
+
192
+ original_height = `identify -format "%h" -ping "#{first_jpeg}"`
193
+ original_width = `identify -format "%w" -ping "#{first_jpeg}"`
194
+ processed_height = (original_height.to_f / original_width.to_f * width).to_i
195
+ height = processed_height
196
+
197
+ # For each jpeg create a cue
198
+ jpegs.each_slice(slice_size) do | jpeg_slice |
199
+ jpeg_slice.each_with_index do |jpeg, index|
200
+ puts "Index: #{index}" if verbose
201
+ cue = {}
202
+
203
+ cue[:start] = start
204
+ cue[:end] = start + opts[:seconds]
205
+
206
+ cue[:x] = ((index % opts[:columns]) * opts[:width])
207
+ puts "x #{cue[:x]}" if verbose
208
+
209
+ cue[:y] = (index.to_f / opts[:columns].to_f).floor * height
210
+ puts "y #{cue[:y]}" if verbose
211
+
212
+ cue[:w] = opts[:width]
213
+ cue[:h] = height
214
+
215
+ start += opts[:seconds]
216
+ puts if verbose
217
+
218
+ cues << cue
219
+ end
220
+ end
221
+
222
+ puts cues if verbose
223
+
224
+ # Create a WebVTT file.
225
+ # TODO: When multiple sprites are created use the correct filename for each sprite.
226
+ # media fragment order: x,y,w,h
227
+ webvtt_file_name = output_file_path + '.vtt'
228
+ puts "Creating WebVTT: #{webvtt_file_name}" if verbose
229
+ File.open(webvtt_file_name, 'w') do |fh|
230
+ fh.puts "WEBVTT\n\n"
231
+
232
+ cues.each_slice(slice_size).with_index do |cue_slice, index|
233
+ cue_slice.each do |cue|
234
+ puts cue if verbose
235
+ start_timestamp = timestamp(cue[:start])
236
+ end_timestamp = timestamp(cue[:end])
237
+ fh.print start_timestamp
238
+ fh.print ' --> '
239
+ fh.puts end_timestamp
240
+
241
+ padded_index = (index + 1).to_s.rjust(5, "0")
242
+
243
+ url = File.join(opts[:url], basename + "-sprite-#{padded_index}.jpg#xywh=#{cue[:x]},#{cue[:y]},#{cue[:w]},#{cue[:h]}")
244
+
245
+ fh.puts url
246
+ fh.puts
247
+ end
248
+ end
249
+
250
+ end
251
+
252
+ end
@@ -0,0 +1,7 @@
1
+ require "video/sprites/version"
2
+
3
+ module Video
4
+ module Sprites
5
+ # Your code goes here...
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Video
2
+ module Sprites
3
+ VERSION = "0.0.1.pre1"
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'video/sprites/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "video-sprites"
8
+ spec.version = Video::Sprites::VERSION
9
+ spec.authors = ["Ashley Blewer, Jay Brown, Jason Ronallo, Nick Zoss"]
10
+ spec.email = ["ashley.blewer@gmail.com, jlb1504@gmail.com, jronallo@gmail.com, nickzoss@yahoo.com"]
11
+ spec.summary = %q{Automatically generated thumbnails for video files.}
12
+ spec.description = %q{Automatically generated thumbnails and WebVTT with time synced media fragment URLs for video files.}
13
+ spec.homepage = "https://github.com/jronallo/video-sprites"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").select{|file| file =~ /^test\/videos\// ? false : true}
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "slop", '~> 3.3', '>= 3.3.3'
23
+ end
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="CompassSettings">
4
+ <option name="compassSupportEnabled" value="true" />
5
+ </component>
6
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
7
+ <exclude-output />
8
+ <content url="file://$MODULE_DIR$">
9
+ <excludeFolder url="file://$MODULE_DIR$/.idea" />
10
+ </content>
11
+ <orderEntry type="inheritedJdk" />
12
+ <orderEntry type="sourceFolder" forTests="false" />
13
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v1.7.3, ruby-2.1.3-p242) [gem]" level="application" />
14
+ <orderEntry type="library" scope="PROVIDED" name="rake (v10.3.2, ruby-2.1.3-p242) [gem]" level="application" />
15
+ <orderEntry type="library" scope="PROVIDED" name="slop (v3.3.3, ruby-2.1.3-p242) [gem]" level="application" />
16
+ </component>
17
+ </module>
18
+
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: video-sprites
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre1
5
+ platform: ruby
6
+ authors:
7
+ - Ashley Blewer, Jay Brown, Jason Ronallo, Nick Zoss
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.3'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.3.3
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '3.3'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.3.3
47
+ description: Automatically generated thumbnails and WebVTT with time synced media
48
+ fragment URLs for video files.
49
+ email:
50
+ - ashley.blewer@gmail.com, jlb1504@gmail.com, jronallo@gmail.com, nickzoss@yahoo.com
51
+ executables:
52
+ - video-sprites
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - ".gitignore"
57
+ - Gemfile
58
+ - LICENSE.txt
59
+ - README.md
60
+ - Rakefile
61
+ - bin/video-sprites
62
+ - lib/video/sprites.rb
63
+ - lib/video/sprites/version.rb
64
+ - video-sprites.gemspec
65
+ - video-sprites.iml
66
+ homepage: https://github.com/jronallo/video-sprites
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">"
82
+ - !ruby/object:Gem::Version
83
+ version: 1.3.1
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.2.2
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Automatically generated thumbnails for video files.
90
+ test_files: []