marcandre-flvedit 0.6.1
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.
- data/CHANGELOG.rdoc +5 -0
- data/LICENSE +24 -0
- data/README.rdoc +90 -0
- data/Rakefile +131 -0
- data/VERSION.yml +4 -0
- data/bin/flvedit +14 -0
- data/lib/flv.rb +24 -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.rb +20 -0
- data/lib/flv/edit/options.rb +162 -0
- data/lib/flv/edit/processor.rb +3 -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/runner.rb +23 -0
- data/lib/flv/edit/version.rb +15 -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/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/lib/flv/edit.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'backports'
|
3
|
+
require_relative '../flv'
|
4
|
+
require_relative 'edit/version'
|
5
|
+
require_relative 'edit/options'
|
6
|
+
require_relative 'edit/processor'
|
7
|
+
require_relative 'edit/runner'
|
8
|
+
|
9
|
+
#todo Change bin/flvedit
|
10
|
+
#todo Auto write to files
|
11
|
+
#todo Command Save no|false
|
12
|
+
#todo add & nearest keyframe & overwrite
|
13
|
+
#todo fix timestamps
|
14
|
+
#todo cut & nearest keyframe?
|
15
|
+
#todo print & xml/yaml & multiple files
|
16
|
+
#todo onLastSecond
|
17
|
+
#todo in|out pipe
|
18
|
+
#todo recursive?
|
19
|
+
#todo fix offset, both 24bit issue & apparent skips
|
20
|
+
#todo bug join & time for audio vs video
|
@@ -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
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module FLV
|
2
|
+
module Edit
|
3
|
+
module Processor
|
4
|
+
# Processors are used to process FLV files. Processors form are chain and the data (header and tags) will 'flow'
|
5
|
+
# through the chain in sequence. Each processor can inspect the data and change the flow,
|
6
|
+
# either by modifying the data, inserting new data in the flow or stopping the propagation of data.
|
7
|
+
#
|
8
|
+
# A FLV file can be seen as a sequence of chunks, the first one being a Header and the following ones
|
9
|
+
# a series of Tags with different types of bodies: Audio, Video or Event. Events store all meta data
|
10
|
+
# related information: onMetaData, onCuePoint, ...
|
11
|
+
#
|
12
|
+
# To tap into the flow of chunks, a processor can define any of the following methods:
|
13
|
+
# on_chunk
|
14
|
+
# on_header
|
15
|
+
# on_tag
|
16
|
+
# on_audio
|
17
|
+
# on_video
|
18
|
+
# on_keyframe
|
19
|
+
# on_interframe
|
20
|
+
# on_disposable_interframe
|
21
|
+
# on_event
|
22
|
+
# on_meta_data
|
23
|
+
# on_cue_point
|
24
|
+
# on_other_event
|
25
|
+
#
|
26
|
+
# All of these methods will have one argument: the current chunk being processed.
|
27
|
+
# For example, if the current chunk is an 'onMetaData' event, then
|
28
|
+
# the following will be called (from the most specialized to the least).
|
29
|
+
# processor.on_meta_data(chunk)
|
30
|
+
# processor.on_event(chunk)
|
31
|
+
# processor.on_chunk(chunk)
|
32
|
+
# # later on, the next processor will handle it:
|
33
|
+
# next_processor.on_meta_data(chunk)
|
34
|
+
# #...
|
35
|
+
#
|
36
|
+
# The methods need not return anything. It is assumed that the chunk will continue to flow through the
|
37
|
+
# processing chain. When the chunk should not continue down the chain, call +absorb+.
|
38
|
+
# To insert other tags in the flow, call +dispatch_instead+.
|
39
|
+
# Finally, it's possible to +stop+ the processing of the file completely.
|
40
|
+
#
|
41
|
+
# It is possible to look back at already processed chunks (up to a certain limit) with +look_back+
|
42
|
+
# or even in the future with +look_ahead+
|
43
|
+
#
|
44
|
+
class Base
|
45
|
+
attr_reader :options
|
46
|
+
|
47
|
+
def initialize(source=nil, options={})
|
48
|
+
@options = options.freeze
|
49
|
+
@source = source
|
50
|
+
on_calls = self.class.instance_methods(false).select{|m| m.to_s.start_with?("on_")}.map(&:to_sym) #Note: to_s needed for ruby 1.9, to_sym for ruby 1.8
|
51
|
+
unless (potential_errors = on_calls - ALL_EVENTS).empty?
|
52
|
+
warn "The following are not events: #{potential_errors.join(',')} (class #{self.class})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def has_next_file?
|
57
|
+
@source.has_next_file?
|
58
|
+
end
|
59
|
+
|
60
|
+
def rewind
|
61
|
+
@source.rewind
|
62
|
+
end
|
63
|
+
|
64
|
+
def absorb(*) # Note: the (*) is so that we can alias events like on_meta_data
|
65
|
+
throw :absorb
|
66
|
+
end
|
67
|
+
|
68
|
+
def stop
|
69
|
+
throw :stop
|
70
|
+
end
|
71
|
+
|
72
|
+
def process_all
|
73
|
+
each{} while has_next_file?
|
74
|
+
end
|
75
|
+
|
76
|
+
def each(&block)
|
77
|
+
return to_enum(:each_chunk) unless block_given?
|
78
|
+
@block = block
|
79
|
+
catch :stop do
|
80
|
+
process_next_file
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_next_file
|
85
|
+
dispatch_chunks(@source)
|
86
|
+
end
|
87
|
+
|
88
|
+
def dispatch_chunks(enum)
|
89
|
+
enum.each do |chunk|
|
90
|
+
dispatch_chunk(chunk)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def dispatch_chunk(chunk)
|
95
|
+
evt = chunk.main_event
|
96
|
+
catch :absorb do
|
97
|
+
EVENT_TRIGGER_LIST[evt].each do |event|
|
98
|
+
send(event, chunk) if respond_to?(event)
|
99
|
+
end
|
100
|
+
@block.call chunk
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def dispatch_instead(*chunks)
|
105
|
+
chunks.each do |chunk|
|
106
|
+
@block.call chunk
|
107
|
+
end
|
108
|
+
absorb
|
109
|
+
end
|
110
|
+
|
111
|
+
def stdout
|
112
|
+
options[:out] || STDOUT
|
113
|
+
end
|
114
|
+
|
115
|
+
EVENT_TRIGGER = {
|
116
|
+
:on_header => :on_chunk,
|
117
|
+
:on_tag => :on_chunk,
|
118
|
+
:on_audio => :on_tag,
|
119
|
+
:on_video => :on_tag,
|
120
|
+
:on_event => :on_tag,
|
121
|
+
:on_meta_data => :on_event,
|
122
|
+
:on_cue_point => :on_event,
|
123
|
+
:on_last_second => :on_event,
|
124
|
+
:on_other_event => :on_event,
|
125
|
+
:on_keyframe => :on_video,
|
126
|
+
:on_interframe => :on_video,
|
127
|
+
:on_disposable_interframe => :on_video
|
128
|
+
}.freeze
|
129
|
+
|
130
|
+
ALL_EVENTS = (EVENT_TRIGGER.keys | EVENT_TRIGGER.values).freeze
|
131
|
+
|
132
|
+
MAIN_EVENTS = ALL_EVENTS.reject{ |k| EVENT_TRIGGER.has_value?(k)}.freeze
|
133
|
+
|
134
|
+
EVENT_TRIGGER_LIST = Hash.new{|h, k| h[k] = [k] + h[EVENT_TRIGGER[k]]}.tap{|h| h[nil] = []; h.values_at(*MAIN_EVENTS)}.freeze
|
135
|
+
|
136
|
+
protected
|
137
|
+
|
138
|
+
def self.desc(text, options = {})
|
139
|
+
registry << [self, text, options]
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.registry # :nodoc:
|
143
|
+
@@registry ||= []
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.absorb(*events)
|
147
|
+
events.each{|evt| alias_method evt, :absorb}
|
148
|
+
end
|
149
|
+
|
150
|
+
end #class Base
|
151
|
+
|
152
|
+
def self.chain(chain_classes, options = {})
|
153
|
+
next_chain_class = chain_classes.pop
|
154
|
+
next_chain_class.new(chain(chain_classes, options), options) if next_chain_class
|
155
|
+
end
|
156
|
+
|
157
|
+
# Let's extend the different kinds of chunks so that any_chunk.main_event returns
|
158
|
+
# the desired trigger.
|
159
|
+
|
160
|
+
module MainEvent # :nodoc:
|
161
|
+
module Header
|
162
|
+
def main_event
|
163
|
+
:on_header
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
module Video
|
168
|
+
MAPPING = {
|
169
|
+
:keyframe => :on_keyframe,
|
170
|
+
:interframe => :on_interframe,
|
171
|
+
:disposable_interframe => :on_disposable_interframe
|
172
|
+
}.freeze
|
173
|
+
|
174
|
+
def main_event
|
175
|
+
MAPPING[frame_type]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
module Audio
|
180
|
+
def main_event
|
181
|
+
:on_audio
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
module Event
|
186
|
+
MAPPING = Hash.new(:on_other_event).merge!(
|
187
|
+
:onMetaData => :on_meta_data,
|
188
|
+
:onCuePoint => :on_cue_point,
|
189
|
+
:onLastSecond => :on_last_second
|
190
|
+
).freeze
|
191
|
+
def main_event
|
192
|
+
MAPPING[event]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
module Tag
|
197
|
+
def main_event
|
198
|
+
body.main_event
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end #module MainEvent
|
202
|
+
end #module Processor
|
203
|
+
end #module Edit
|
204
|
+
|
205
|
+
[Header, Tag, Audio, Video, Event].each do |klass|
|
206
|
+
klass.class_eval{ include Edit::Processor::MainEvent.const_get(klass.to_s.sub('FLV::', '')) }
|
207
|
+
end
|
208
|
+
|
209
|
+
end #module FLV
|