video-sprites 0.0.1.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []