flvedit 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +5 -0
- data/LICENSE +24 -0
- data/README.rdoc +90 -0
- data/Rakefile +137 -0
- data/VERSION.yml +4 -0
- data/bin/flvedit +14 -0
- data/lib/flv/audio.rb +66 -0
- data/lib/flv/base.rb +38 -0
- data/lib/flv/body.rb +57 -0
- data/lib/flv/edit/options.rb +162 -0
- data/lib/flv/edit/processor/add.rb +67 -0
- data/lib/flv/edit/processor/base.rb +209 -0
- data/lib/flv/edit/processor/command_line.rb +23 -0
- data/lib/flv/edit/processor/cut.rb +27 -0
- data/lib/flv/edit/processor/debug.rb +30 -0
- data/lib/flv/edit/processor/head.rb +16 -0
- data/lib/flv/edit/processor/join.rb +52 -0
- data/lib/flv/edit/processor/meta_data_maker.rb +127 -0
- data/lib/flv/edit/processor/print.rb +13 -0
- data/lib/flv/edit/processor/printer.rb +27 -0
- data/lib/flv/edit/processor/reader.rb +30 -0
- data/lib/flv/edit/processor/save.rb +28 -0
- data/lib/flv/edit/processor/update.rb +27 -0
- data/lib/flv/edit/processor.rb +3 -0
- data/lib/flv/edit/runner.rb +23 -0
- data/lib/flv/edit/version.rb +15 -0
- data/lib/flv/edit.rb +20 -0
- data/lib/flv/event.rb +40 -0
- data/lib/flv/file.rb +41 -0
- data/lib/flv/header.rb +37 -0
- data/lib/flv/packing.rb +140 -0
- data/lib/flv/tag.rb +62 -0
- data/lib/flv/timestamp.rb +124 -0
- data/lib/flv/util/double_check.rb +22 -0
- data/lib/flv/video.rb +73 -0
- data/lib/flv.rb +24 -0
- data/test/fixtures/corrupted.flv +0 -0
- data/test/fixtures/short.flv +0 -0
- data/test/fixtures/tags.xml +39 -0
- data/test/test_flv.rb +145 -0
- data/test/test_flv_edit.rb +32 -0
- data/test/test_flv_edit_results.rb +27 -0
- data/test/test_helper.rb +9 -0
- data/test/text_flv_edit_results/add_tags.txt +132 -0
- data/test/text_flv_edit_results/cut_from.txt +114 -0
- data/test/text_flv_edit_results/cut_key.txt +20 -0
- data/test/text_flv_edit_results/debug.txt +132 -0
- data/test/text_flv_edit_results/debug_limited.txt +18 -0
- data/test/text_flv_edit_results/debug_range.txt +32 -0
- data/test/text_flv_edit_results/join.txt +237 -0
- data/test/text_flv_edit_results/print.txt +16 -0
- data/test/text_flv_edit_results/stop.txt +38 -0
- data/test/text_flv_edit_results/update.txt +33 -0
- metadata +134 -0
data/CHANGELOG.rdoc
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
FLVEdit - Copyright (c) 2009 Marc-André Lafortune
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions
|
6
|
+
are met:
|
7
|
+
1. Redistributions of source code must retain the above copyright
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
notice, this list of conditions and the following disclaimer in the
|
11
|
+
documentation and/or other materials provided with the distribution.
|
12
|
+
3. The name of the author may not be used to endorse or promote products
|
13
|
+
derived from this software without specific prior written permission.
|
14
|
+
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
18
|
+
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
19
|
+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
20
|
+
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
21
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
22
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
24
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
= FLVEdit - Flash video manipulation
|
2
|
+
|
3
|
+
FLVEdit is a tool that will read or edit FLV files to:
|
4
|
+
* generate meta data
|
5
|
+
* add/remove cue points
|
6
|
+
* join files
|
7
|
+
* inspect files
|
8
|
+
|
9
|
+
It is meant as an improved FLVTool2, fixing the shortfalls that prompted others to write FLVTool++ & FLVMeta (see comparison chart)
|
10
|
+
|
11
|
+
FLVEdit can be used either as a command-line command or as a ruby library.
|
12
|
+
|
13
|
+
== This is not a stable release
|
14
|
+
<b>Warning:</b> The basic functionality is there, but I'll be improving features and documentation. Until version 1.0, there will most likely be many changes to the command line interface and the library API.
|
15
|
+
|
16
|
+
Comments & requests welcome...
|
17
|
+
|
18
|
+
== Installation
|
19
|
+
|
20
|
+
+flvedit+ is a gem mirrored on Rubyforge and can thus be installed with:
|
21
|
+
|
22
|
+
sudo gem install flvedit
|
23
|
+
|
24
|
+
flvedit is compatible with Ruby 1.8 and 1.9
|
25
|
+
|
26
|
+
== Command-line tool
|
27
|
+
|
28
|
+
Type 'flvedit' for description of commands.
|
29
|
+
|
30
|
+
== Library
|
31
|
+
|
32
|
+
FLVEdit is written in ruby and divided in two layers.
|
33
|
+
|
34
|
+
=== FLV file format
|
35
|
+
The FLV layer handles the FLV file format. It makes reading and writing FLV files a breeze:
|
36
|
+
|
37
|
+
FLV::File.open("example.flv") {|f| f.to_a } # ==> [<#FLV::Header...>, <#FLV::Tag...>, ...]
|
38
|
+
|
39
|
+
The main class is FLV::Tag with its different possible bodies: FLV::Audio, FLV::Video and most importantly FLV::Event for all meta data related information (onMetaData, onCuePoint, ...)
|
40
|
+
|
41
|
+
The data packing and unpacking relies on the packable[http://github.com/marcandre/packable] library.
|
42
|
+
|
43
|
+
=== FLV::Edit tool
|
44
|
+
|
45
|
+
The FLV::Edit layer is the command-line tool itself. The FLV::Edit::Runner class parses the options and builds a chain of processors to apply to some flv files. Processors all derive from FLV::Edit::Processor::Base and only need to specify what to do for the type of data it wants to process. A simplistic example to use this level:
|
46
|
+
|
47
|
+
class CountCuePoints < FLV::Edit::Processor::Base
|
48
|
+
attr_writer :count
|
49
|
+
def on_cue_point(cue)
|
50
|
+
@count ||= 0
|
51
|
+
@count += 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Call manually
|
56
|
+
count =
|
57
|
+
FLV::File.open("example.flv") do |f|
|
58
|
+
CountCuePoints.new(f).process_all.count
|
59
|
+
end
|
60
|
+
|
61
|
+
# Chain with other commands:
|
62
|
+
count = FLV::Edit::Runner.new([CountCuePoints, FLV::Edit::Processor::Debug], :files => "example.swf").run.count
|
63
|
+
|
64
|
+
See FLV::Edit::Processor::Base for details on how the processing works
|
65
|
+
|
66
|
+
== Comparisons with existing tools
|
67
|
+
|
68
|
+
=== FLVTool2
|
69
|
+
|
70
|
+
Features:
|
71
|
+
* Can join (concat) flv files
|
72
|
+
* Won't load the whole files in memory all at once
|
73
|
+
* Won't choke on read-only files
|
74
|
+
* Supports extented timestamps (for flv over... 4 hours!)
|
75
|
+
|
76
|
+
Code:
|
77
|
+
* Complete rewrite
|
78
|
+
* More ruby-oriented
|
79
|
+
* Commented
|
80
|
+
* Unit tests
|
81
|
+
* Easier to use library
|
82
|
+
* Easily expandable with your own processing
|
83
|
+
|
84
|
+
=== Compared to FLVTool++
|
85
|
+
* Handles cue points
|
86
|
+
* Usable as a library
|
87
|
+
<to be completed>
|
88
|
+
|
89
|
+
=== Compared to FLVMeta
|
90
|
+
<to be completed>
|
data/Rakefile
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rcov/rcovtask'
|
6
|
+
|
7
|
+
# bench
|
8
|
+
begin
|
9
|
+
desc "Benchmark"
|
10
|
+
task :bench do
|
11
|
+
require File.dirname(__FILE__)+"/lib/flv/edit"
|
12
|
+
SHORT_FLV = File.dirname(__FILE__) + "/test/fixtures/short.flv"
|
13
|
+
require 'benchmark'
|
14
|
+
include Benchmark
|
15
|
+
runner = FLV::Edit::Runner.new([SHORT_FLV, SHORT_FLV, '--Join'])
|
16
|
+
bm(6) do |x|
|
17
|
+
x.report("test") { 20.times { runner.run } }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
begin
|
24
|
+
require 'jeweler'
|
25
|
+
Jeweler::Tasks.new do |gem|
|
26
|
+
gem.name = "flvedit"
|
27
|
+
gem.summary = "Command line tool & library to handle FLV files"
|
28
|
+
gem.email = "github@marc-andre.ca"
|
29
|
+
gem.homepage = "http://github.com/marcandre/flvedit"
|
30
|
+
gem.description = <<-EOS
|
31
|
+
flvedit allows you to:
|
32
|
+
* compute metadata for FLV files
|
33
|
+
* merge, split or cut FLVs
|
34
|
+
* insert / remote cue points or other events
|
35
|
+
|
36
|
+
flvedit is meant as a replacement for FLVTool2, FLVMeta, FLVTool++
|
37
|
+
It can be used as a command line tool or as a Ruby library.
|
38
|
+
EOS
|
39
|
+
gem.authors = ["Marc-André Lafortune"]
|
40
|
+
gem.rubyforge_project = "flvedit"
|
41
|
+
gem.add_dependency "packable", ">=1.2"
|
42
|
+
gem.add_dependency "backports"
|
43
|
+
gem.has_rdoc = true
|
44
|
+
gem.rdoc_options << '--title' << 'FLV::Edit' <<
|
45
|
+
'--main' << 'README.rdoc' <<
|
46
|
+
'--line-numbers' << '--inline-source'
|
47
|
+
end
|
48
|
+
rescue LoadError
|
49
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
50
|
+
end unless RUBY_VERSION >= "1.9"
|
51
|
+
|
52
|
+
Rake::TestTask.new do |t|
|
53
|
+
t.libs << 'lib'
|
54
|
+
t.pattern = 'test/**/test_*.rb'
|
55
|
+
t.verbose = false
|
56
|
+
end
|
57
|
+
|
58
|
+
Rake::RDocTask.new do |rdoc|
|
59
|
+
if File.exist?('VERSION.yml')
|
60
|
+
config = YAML.load(File.read('VERSION.yml'))
|
61
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
62
|
+
else
|
63
|
+
version = ""
|
64
|
+
end
|
65
|
+
rdoc.rdoc_dir = 'rdoc'
|
66
|
+
rdoc.title = "FLVEdit #{version}"
|
67
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
68
|
+
rdoc.rdoc_files.include('README*')
|
69
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
70
|
+
end
|
71
|
+
|
72
|
+
Rcov::RcovTask.new do |t|
|
73
|
+
t.libs << 'test'
|
74
|
+
t.test_files = FileList['test/**/*_test.rb']
|
75
|
+
t.verbose = true
|
76
|
+
end
|
77
|
+
|
78
|
+
task :default => :rcov
|
79
|
+
|
80
|
+
# stats
|
81
|
+
begin
|
82
|
+
gem 'rails'
|
83
|
+
require 'code_statistics'
|
84
|
+
namespace :spec do
|
85
|
+
desc "Use Rails's rake:stats task for a gem"
|
86
|
+
task :statsetup do
|
87
|
+
class CodeStatistics
|
88
|
+
def calculate_statistics
|
89
|
+
@pairs.inject({}) do |stats, pair|
|
90
|
+
if 3 == pair.size
|
91
|
+
stats[pair.first] = calculate_directory_statistics(pair[1], pair[2]); stats
|
92
|
+
else
|
93
|
+
stats[pair.first] = calculate_directory_statistics(pair.last); stats
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
::STATS_DIRECTORIES = [['Libraries', 'lib', /.(sql|rhtml|erb|rb|yml)$/],
|
99
|
+
['Tests', 'test', /.(sql|rhtml|erb|rb|yml)$/]]
|
100
|
+
::CodeStatistics::TEST_TYPES << "Tests"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
desc "Report code statistics (KLOCs, etc) from the application"
|
104
|
+
task :stats => "spec:statsetup" do
|
105
|
+
CodeStatistics.new(*STATS_DIRECTORIES).to_s
|
106
|
+
end
|
107
|
+
rescue Gem::LoadError => le
|
108
|
+
task :stats do
|
109
|
+
raise RuntimeError, "‘rails’ gem not found - you must install it in order to use this task.n"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
begin
|
114
|
+
require 'rake/contrib/sshpublisher'
|
115
|
+
namespace :rubyforge do
|
116
|
+
|
117
|
+
desc "Release gem and RDoc documentation to RubyForge"
|
118
|
+
task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
|
119
|
+
|
120
|
+
namespace :release do
|
121
|
+
desc "Publish RDoc to RubyForge."
|
122
|
+
task :docs => [:rdoc] do
|
123
|
+
config = YAML.load(
|
124
|
+
File.read(File.expand_path('~/.rubyforge/user-config.yml'))
|
125
|
+
)
|
126
|
+
|
127
|
+
host = "#{config['username']}@rubyforge.org"
|
128
|
+
remote_dir = "/var/www/gforge-projects/flvedit/"
|
129
|
+
local_dir = 'rdoc'
|
130
|
+
|
131
|
+
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
rescue LoadError
|
136
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
137
|
+
end
|
data/VERSION.yml
ADDED
data/bin/flvedit
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright (c) 2009 Marc-Andre Lafortune
|
5
|
+
# Release under the (modified) BSD License (see gem's LICENSE file)
|
6
|
+
#++
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'flv/edit'
|
10
|
+
rescue LoadError
|
11
|
+
require 'rubygems'
|
12
|
+
require 'flv/edit'
|
13
|
+
end
|
14
|
+
FLV::Edit::Runner.new(ARGV).run
|
data/lib/flv/audio.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module FLV
|
2
|
+
# The body of an audio tag.
|
3
|
+
# The data is quite complex stuff. We make no attempt to understand it all
|
4
|
+
# or to be able to modify any of it. We simply consider it a complex string
|
5
|
+
# and read the interesting bits to give more info.
|
6
|
+
class Audio < String
|
7
|
+
include Body
|
8
|
+
|
9
|
+
FORMATS = Hash.new{|h, key| "Unknown audio format: #{key}"}.merge(
|
10
|
+
0 => :"Linear PCM, platform endian" ,
|
11
|
+
1 => :ADPCM ,
|
12
|
+
2 => :MP3 ,
|
13
|
+
3 => :"Linear PCM, little endian" ,
|
14
|
+
4 => :"Nellymoser 16-kHz mono" ,
|
15
|
+
5 => :"Nellymoser 8-kHz mono" ,
|
16
|
+
6 => :Nellymoser ,
|
17
|
+
7 => :"G.711 A-law logarithmic PCM" ,
|
18
|
+
8 => :"G.711 mu-law logarithmic PCM" ,
|
19
|
+
10 => :AAC ,
|
20
|
+
11 => :Speex ,
|
21
|
+
14 => :"MP3 8-kHz" ,
|
22
|
+
15 => :"Device-specific sound"
|
23
|
+
).freeze
|
24
|
+
|
25
|
+
EXCEPTIONS = Hash.new({}).merge(
|
26
|
+
:"Nellymoser 8-kHz mono" => {:channel => :mono, :rate => 8000},
|
27
|
+
:"Nellymoser 16-kHz mono" => {:channel => :mono, :rate => 16000},
|
28
|
+
:AAC => {:channel => :stereo,:rate => 44000},
|
29
|
+
:"MP3 8-kHz" => {:rate => 8000}
|
30
|
+
).freeze
|
31
|
+
|
32
|
+
CHANNELS = { 0 => :mono, 1 => :stereo}.freeze
|
33
|
+
|
34
|
+
def codec_id
|
35
|
+
read_bits(0..3)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the format (see Audio::FORMATS for list)
|
39
|
+
def format
|
40
|
+
FORMATS[codec_id]
|
41
|
+
end
|
42
|
+
|
43
|
+
# returns :mono or :stereo
|
44
|
+
def channel
|
45
|
+
EXCEPTIONS[format][:channel] ||
|
46
|
+
CHANNELS[read_bits(7)]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the sampling rate (in Hz)
|
50
|
+
def rate
|
51
|
+
EXCEPTIONS[format][:rate] ||
|
52
|
+
5500 << read_bits(4..5)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the sample size (in bits)
|
56
|
+
def sample_size
|
57
|
+
EXCEPTIONS[format][:sample_size] ||
|
58
|
+
8 << read_bits(6)
|
59
|
+
end
|
60
|
+
|
61
|
+
def is?(what)
|
62
|
+
format.to_s.downcase == what.to_s.downcase || super
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
data/lib/flv/base.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module FLV
|
2
|
+
# Common fonctionality to both FLV::Header, FLV::Tag & FLV::Body
|
3
|
+
module Base
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
include Packable
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns the instance methods
|
11
|
+
# that are proper (i.e. not redefinitions)
|
12
|
+
# and that don't require any argument.
|
13
|
+
def getters(of=self)
|
14
|
+
of.class.ancestors.
|
15
|
+
map{|k| k.instance_methods(false)}.
|
16
|
+
inject{|proper, methods| proper -= methods}. # tricky: first ancestor is of.class, so that's what we start with
|
17
|
+
select{|m| of.class.instance_method(m).arity.between?(-1,0)}
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h(attributes = getters)
|
21
|
+
Hash[attributes.map do |a|
|
22
|
+
a = a.to_s.delete("@").to_sym
|
23
|
+
[a, send(a)]
|
24
|
+
end]
|
25
|
+
end
|
26
|
+
|
27
|
+
def is?(what)
|
28
|
+
kn = self.class.name.downcase
|
29
|
+
[kn, kn.sub("flv::","")].include?(what.to_s.downcase)
|
30
|
+
end
|
31
|
+
|
32
|
+
def size
|
33
|
+
StringIO.new.packed.write(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/flv/body.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module FLV
|
2
|
+
|
3
|
+
# Common fonctionality to all types of Bodies (FLV::Audio, FLV::Video & FLV::Event)
|
4
|
+
module Body
|
5
|
+
def self.included(base)
|
6
|
+
# Caution: order is important; InstanceMethods::is? relies on Base::is?
|
7
|
+
base.class_eval do
|
8
|
+
include Base
|
9
|
+
include InstanceMethods
|
10
|
+
include InstanceMethodsWhenString
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods # :nodoc:
|
15
|
+
def debug(format, *) #:nodoc
|
16
|
+
format.values(to_h)
|
17
|
+
end
|
18
|
+
|
19
|
+
def is?(what)
|
20
|
+
case what
|
21
|
+
when String, Symbol
|
22
|
+
super(what.to_s.downcase.gsub!(/_tag$/, "") || :never_match_on_class_name_unless_string_ends_with_tag)
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def similar_to?(other_body)
|
29
|
+
getters.each{|getter| return false unless send(getter) == other_body.send(getter)}
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def title
|
34
|
+
self.class.name + " tag"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module InstanceMethodsWhenString # :nodoc:
|
39
|
+
# Returns an +Integer+ computed from bits specified by +which+.
|
40
|
+
# The 0th bit is the most significant bit of the first character.
|
41
|
+
# +which+ can designate a range of bits or a single bit
|
42
|
+
def read_bits(which)
|
43
|
+
which = which..which if which.is_a? Integer
|
44
|
+
first_byte, last_byte = which.first >> 3, which.max >> 3
|
45
|
+
return (getbyte(first_byte) >> (7 & ~which.max)) & ~(~1 << which.max-which.first) if(first_byte == last_byte)
|
46
|
+
mid = last_byte << 3
|
47
|
+
read_bits(which.first...mid) << (which.max - mid + 1) | read_bits(mid..which.max)
|
48
|
+
end
|
49
|
+
|
50
|
+
# We need to redefine this, since we want to end up with a Body, not a String
|
51
|
+
def read_packed(io, options) #:nodoc:
|
52
|
+
replace(io.read(String, options))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module FLV
|
5
|
+
module Edit
|
6
|
+
class Options
|
7
|
+
attr_reader :commands, :options
|
8
|
+
def initialize(argv)
|
9
|
+
@commands, @options = parse(argv)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_a
|
13
|
+
[@commands, @options]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def parse(argv)
|
18
|
+
commands = []
|
19
|
+
options = {}
|
20
|
+
parser = OptionParser.new do |parser|
|
21
|
+
parser.banner = banner
|
22
|
+
parser.separator ""
|
23
|
+
parser.separator "Commands:"
|
24
|
+
Processor::Base.registry.sort_by(&:to_s).
|
25
|
+
each do |command, desc, cmd_opt|
|
26
|
+
name = command.name.split('::').last.downcase
|
27
|
+
option = name.downcase.to_sym
|
28
|
+
shortcut = cmd_opt[:shortcut] || name[0..0]
|
29
|
+
if param = cmd_opt[:param]
|
30
|
+
name << " " << (param[:name] || param[:class].to_s.upcase)
|
31
|
+
desc = [param[:class], *desc] if param[:class]
|
32
|
+
end
|
33
|
+
parser.on("-#{shortcut}", "--#{name}", *desc) do |v|
|
34
|
+
options[option] = v
|
35
|
+
commands << command
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
parser.separator ""
|
40
|
+
parser.separator "Switches:"
|
41
|
+
[
|
42
|
+
[:keyframe_mode, "Keyframe mode slides on_cue_point(navigation) tags added by the",
|
43
|
+
"add command to nearest keyframe position"]
|
44
|
+
].each do |switch, *desc|
|
45
|
+
shortcut = desc.first.is_a?(Symbol) ? desc.shift.to_s : switch.to_s[0..0]
|
46
|
+
full = desc.first.is_a?(Class) ? "--#{switch.to_s} N" : "--#{switch.to_s}"
|
47
|
+
parser.on("-#{shortcut}", full, *desc) { |v| options[switch] = v }
|
48
|
+
end
|
49
|
+
|
50
|
+
parser.separator "Common:"
|
51
|
+
parser.on("-h", "--help", "Show this message") {}
|
52
|
+
parser.on("--version", "Show version") do
|
53
|
+
puts "Current version: #{FLV::Edit.version}"
|
54
|
+
exit
|
55
|
+
end
|
56
|
+
|
57
|
+
parser.separator ""
|
58
|
+
parser.separator "flvedit (#{FLV::Edit.version}), copyright (c) 2009 Marc-André Lafortune"
|
59
|
+
parser.separator "This program is published under the BSD license."
|
60
|
+
end
|
61
|
+
options[:files] = parser.parse!(argv)
|
62
|
+
if commands.empty?
|
63
|
+
puts parser
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
return commands, options
|
67
|
+
end
|
68
|
+
|
69
|
+
def banner
|
70
|
+
<<-EOS
|
71
|
+
Usage: flvedit#{(RUBY_PLATFORM =~ /win32/) ? '.exe' : ''} [--input] files --processing --more_processing ... [--save [PATH]]
|
72
|
+
******
|
73
|
+
*NOTE*: THIS IS NOT A STABLE RELEASE. API WILL CHANGE!
|
74
|
+
******
|
75
|
+
flvedit will apply the given processing to the input files.
|
76
|
+
|
77
|
+
Examples:
|
78
|
+
# Printout of the first 42 tags:
|
79
|
+
flvedit example.flv --debug 42
|
80
|
+
|
81
|
+
# Extract the first 2 seconds of a.flv and b.flv, join them and update the meta data.
|
82
|
+
flvedit a.flv b.flv --cut 0-2 --join --update --save out.flv
|
83
|
+
|
84
|
+
Option formats:
|
85
|
+
TIMESTAMP: Given in seconds with decimals for fractions of seconds.
|
86
|
+
Use 'h' and 'm' or ':' for hours and minutes separators.
|
87
|
+
Examples: 1:00:01 (for 1 hour and 1 second), 1h1 (same),
|
88
|
+
2m3.004 (2 minutes, 3 seconds and 4 milliseconds), 123.004 (same)
|
89
|
+
RANGE: Expressed as BEGIN-END, where BEGIN or END are timestamps
|
90
|
+
that can be let empty for no limit
|
91
|
+
Examples: 1m-2m (second minute), 1m- (all but first minute), -1m (first minute)
|
92
|
+
RANGE/TS: RANGE or TIMESTAMP.
|
93
|
+
EOS
|
94
|
+
#s.split("\n").collect!(&:strip).join("\n")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#todo: explicit Input command
|
101
|
+
#todo: conditions for implicit Save
|
102
|
+
|
103
|
+
# options[:metadatacreator] = "inlet media FLVTool2 v#{PROGRAMM_VERSION.join('.')} - http://www.inlet-media.de/flvtool2"
|
104
|
+
# options[:metadata] = {}
|
105
|
+
|
106
|
+
# when /^-([a-zA-Z0-9]+?):(.+)$/
|
107
|
+
# options[:metadata][$1] = $2
|
108
|
+
# when /^-([a-zA-Z0-9]+?)#([0-9]+)$/
|
109
|
+
# options[:metadata][$1] = $2.to_f
|
110
|
+
# when /^-([a-zA-Z0-9]+?)@(\d{4,})-(\d{2,})-(\d{2,}) (\d{2,}):(\d{2,}):(\d{2,})$/
|
111
|
+
# options[:metadata][$1] = Time.local($2, $3, $4, $5, $6, $7)
|
112
|
+
# when /^([^-].*)$/
|
113
|
+
# if options[:in_path].nil?
|
114
|
+
# options[:in_path] = $1
|
115
|
+
# if options[:in_path].downcase =~ /stdin|pipe/
|
116
|
+
# options[:in_pipe] = true
|
117
|
+
# options[:in_path] = 'pipe'
|
118
|
+
# else
|
119
|
+
# options[:in_path] = File.expand_path( options[:in_path] )
|
120
|
+
# end
|
121
|
+
# else
|
122
|
+
# options[:out_path] = $1
|
123
|
+
# if options[:out_path].downcase =~ /stdout|pipe/
|
124
|
+
# options[:out_pipe] = true
|
125
|
+
# options[:out_path] = 'pipe'
|
126
|
+
# else
|
127
|
+
# options[:out_path] = File.expand_path( options[:out_path] )
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
|
133
|
+
|
134
|
+
# def self.validate_options( options )
|
135
|
+
# if options[:commands].empty?
|
136
|
+
# show_usage
|
137
|
+
# exit 0
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# options[:commands].each do |command|
|
141
|
+
# case command
|
142
|
+
# when :print
|
143
|
+
# if options[:out_pipe]
|
144
|
+
# throw_error "Could not use print command in conjunction with output piping or redirection"
|
145
|
+
# exit 1
|
146
|
+
# end
|
147
|
+
# when :debug
|
148
|
+
# if options[:out_pipe]
|
149
|
+
# throw_error "Could not use debug command in conjunction with output piping or redirection"
|
150
|
+
# exit 1
|
151
|
+
# end
|
152
|
+
# when :help
|
153
|
+
# show_usage
|
154
|
+
# exit 0
|
155
|
+
# when :version
|
156
|
+
# show_version
|
157
|
+
# exit 0
|
158
|
+
# end
|
159
|
+
# end
|
160
|
+
# options
|
161
|
+
# end
|
162
|
+
#
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module FLV
|
6
|
+
module Edit
|
7
|
+
module Processor
|
8
|
+
|
9
|
+
class Add < Base
|
10
|
+
desc "Adds tags from the xml or yaml file PATH (default: tags.yaml/xml)", :param => {:name => "[PATH]"}
|
11
|
+
|
12
|
+
def initialize(*)
|
13
|
+
super
|
14
|
+
@add = (options[:add_tags] || read_tag_file).sort_by(&:timestamp)
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_tag(tag)
|
18
|
+
insert_before tag, @add
|
19
|
+
end
|
20
|
+
|
21
|
+
# def on_keyframe(tag)
|
22
|
+
# insert_before tag, @add_before_key_frame
|
23
|
+
# end
|
24
|
+
|
25
|
+
private
|
26
|
+
def insert_before(tag, list_to_insert)
|
27
|
+
stop_at = list_to_insert.find_index{|i| i.timestamp > tag.timestamp} || list_to_insert.length
|
28
|
+
dispatch_instead(*(list_to_insert.slice!(0...stop_at) << tag)) if stop_at > 0
|
29
|
+
end
|
30
|
+
|
31
|
+
DEFAULT = ["tags.yaml", "tags.xml"]
|
32
|
+
#todo: overwrite, navigation
|
33
|
+
#todo: default event
|
34
|
+
def read_tag_file
|
35
|
+
filename = options[:add]
|
36
|
+
DEFAULT.each{|fn| filename ||= fn if ::File.exists?(fn)}
|
37
|
+
raise "You must either specify a tag file or have a file named #{DEFAULT.join(' or ')}" unless filename
|
38
|
+
::File.open(filename) {|f| read_tags(f)}
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_tags(file)
|
42
|
+
tags =
|
43
|
+
unless file.path.downcase.end_with? ".xml"
|
44
|
+
YAML.load(file)
|
45
|
+
else
|
46
|
+
xml = REXML::Document.new(file, :ignore_whitespace_nodes => :all)
|
47
|
+
xml.root.elements.to_a("metatag").map do |tag|
|
48
|
+
{
|
49
|
+
:event => tag.attributes['event'],
|
50
|
+
:overwrite => tag.attributes['overwrite'],
|
51
|
+
:timestamp => (tag.elements['timestamp'].text rescue 0),
|
52
|
+
:navigation => (tag.elements['type'].text == "navigation" rescue false),
|
53
|
+
:arguments => Hash[tag.elements['parameters'].map{|param| [param.name, param.text]}]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
tags = [tags] unless tags.is_a? Array
|
58
|
+
tags.map do |info|
|
59
|
+
info.symbolize_keys!
|
60
|
+
Tag.new(info[:timestamp], Event.new(info[:event], info[:arguments]))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|