flvedit 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG.rdoc +5 -0
  2. data/LICENSE +24 -0
  3. data/README.rdoc +90 -0
  4. data/Rakefile +137 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/flvedit +14 -0
  7. data/lib/flv/audio.rb +66 -0
  8. data/lib/flv/base.rb +38 -0
  9. data/lib/flv/body.rb +57 -0
  10. data/lib/flv/edit/options.rb +162 -0
  11. data/lib/flv/edit/processor/add.rb +67 -0
  12. data/lib/flv/edit/processor/base.rb +209 -0
  13. data/lib/flv/edit/processor/command_line.rb +23 -0
  14. data/lib/flv/edit/processor/cut.rb +27 -0
  15. data/lib/flv/edit/processor/debug.rb +30 -0
  16. data/lib/flv/edit/processor/head.rb +16 -0
  17. data/lib/flv/edit/processor/join.rb +52 -0
  18. data/lib/flv/edit/processor/meta_data_maker.rb +127 -0
  19. data/lib/flv/edit/processor/print.rb +13 -0
  20. data/lib/flv/edit/processor/printer.rb +27 -0
  21. data/lib/flv/edit/processor/reader.rb +30 -0
  22. data/lib/flv/edit/processor/save.rb +28 -0
  23. data/lib/flv/edit/processor/update.rb +27 -0
  24. data/lib/flv/edit/processor.rb +3 -0
  25. data/lib/flv/edit/runner.rb +23 -0
  26. data/lib/flv/edit/version.rb +15 -0
  27. data/lib/flv/edit.rb +20 -0
  28. data/lib/flv/event.rb +40 -0
  29. data/lib/flv/file.rb +41 -0
  30. data/lib/flv/header.rb +37 -0
  31. data/lib/flv/packing.rb +140 -0
  32. data/lib/flv/tag.rb +62 -0
  33. data/lib/flv/timestamp.rb +124 -0
  34. data/lib/flv/util/double_check.rb +22 -0
  35. data/lib/flv/video.rb +73 -0
  36. data/lib/flv.rb +24 -0
  37. data/test/fixtures/corrupted.flv +0 -0
  38. data/test/fixtures/short.flv +0 -0
  39. data/test/fixtures/tags.xml +39 -0
  40. data/test/test_flv.rb +145 -0
  41. data/test/test_flv_edit.rb +32 -0
  42. data/test/test_flv_edit_results.rb +27 -0
  43. data/test/test_helper.rb +9 -0
  44. data/test/text_flv_edit_results/add_tags.txt +132 -0
  45. data/test/text_flv_edit_results/cut_from.txt +114 -0
  46. data/test/text_flv_edit_results/cut_key.txt +20 -0
  47. data/test/text_flv_edit_results/debug.txt +132 -0
  48. data/test/text_flv_edit_results/debug_limited.txt +18 -0
  49. data/test/text_flv_edit_results/debug_range.txt +32 -0
  50. data/test/text_flv_edit_results/join.txt +237 -0
  51. data/test/text_flv_edit_results/print.txt +16 -0
  52. data/test/text_flv_edit_results/stop.txt +38 -0
  53. data/test/text_flv_edit_results/update.txt +33 -0
  54. metadata +134 -0
@@ -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
@@ -0,0 +1,23 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+ class CommandLine < Base
5
+ def on_header(h)
6
+ @last = h.path
7
+ end
8
+
9
+ def process_all
10
+ ok, errors = [], []
11
+ begin
12
+ each{}
13
+ ok << @last
14
+ rescue Exception => e
15
+ errors << [@last, e]
16
+ end while has_next_file?
17
+ puts (["Processed successfully:"] + ok).join("\n") unless ok.empty?
18
+ puts (["**** Processed with errors: ****"] + errors.map{|path, err| "#{path}: #{err}"}).join("\n") unless errors.empty?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+ class Cut < Base
5
+ desc ["Cuts file using the given RANGE"],
6
+ :param => {:class => TimestampRange, :name => "RANGE"}, :shortcut => "x"
7
+
8
+ def on_header(*)
9
+ @from, @to = options[:cut].begin, options[:cut].end
10
+ @wait_for_keyframe = options[:keyframe_mode]
11
+ @first_timestamp = nil
12
+ end
13
+
14
+ def on_tag(tag)
15
+ if tag.timestamp > @to
16
+ stop
17
+ elsif (tag.timestamp < @from) || (@wait_for_keyframe &&= !tag.body.is?(:keyframe))
18
+ absorb
19
+ else
20
+ @first_timestamp ||= tag.timestamp
21
+ tag.timestamp -= @first_timestamp
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "printer"
2
+
3
+ module FLV
4
+ module Edit
5
+ module Processor
6
+ class Debug < Base
7
+ desc ["Prints out the details of all tags. Information that stays the same",
8
+ "from one tag type to the next will not be repeated.",
9
+ "A RANGE argument will limit the output to tags within that range;",
10
+ "similarily, a given TIMESTAMP will limit the output to tags",
11
+ "within 0.1s of this timestamp."],
12
+ :param => {:class => TimestampOrTimestampRange, :name => "[RANGE/TS]"}
13
+ def on_header(tag)
14
+ @range = self.options[:debug] || TimestampRange.new(0, INFINITY)
15
+ @range = @range.widen(0.1) unless @range.is_a? Range
16
+ @last = {}
17
+ @printer = Printer.new(stdout)
18
+ tag.debug(@printer) if @range.include? 0
19
+ end
20
+
21
+ def on_tag(tag)
22
+ return unless @range.include? tag.timestamp
23
+ tag.debug(@printer, @last[tag.body.class])
24
+ @last[tag.body.class] = tag
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+ class Head < Base
5
+ desc "Processes only the first NB tags.", :param => {:class => Integer, :name => "NB"}, :shortcut => "n"
6
+ def on_header(header)
7
+ @count = self.options[:head]
8
+ end
9
+
10
+ def on_tag(tag)
11
+ throw :stop if (@count -= 1) < 0
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+
5
+ class Join < Base
6
+ desc "Join the FLV files"
7
+
8
+ def process_next_file
9
+ dispatch_chunks(@source) while @source.has_next_file?
10
+ end
11
+
12
+ def on_tag(tag)
13
+ if @wait_for_keyframe
14
+ absorb
15
+ else
16
+ tag.timestamp += @delta
17
+ end
18
+ @last_timestamp = tag.timestamp
19
+ end
20
+
21
+ def on_video(tag)
22
+ @next_to_last_video_timestamp, @last_video_timestamp = @last_video_timestamp, tag.timestamp
23
+ end
24
+
25
+ def on_keyframe(tag)
26
+ if @wait_for_keyframe
27
+ @wait_for_keyframe = false
28
+ @delta -= tag.timestamp
29
+ end
30
+ end
31
+
32
+ def on_header(h)
33
+ if is_first_header = !@delta
34
+ @delta = 0
35
+ @wait_for_keyframe = false
36
+ @last_video_timestamp = 0
37
+ else
38
+ if @last_video_timestamp
39
+ last_interval = @last_video_timestamp - @next_to_last_video_timestamp
40
+ @delta += [@last_video_timestamp + last_interval, @last_timestamp].max
41
+ @wait_for_keyframe = true
42
+ else
43
+ @delta = @last_timestamp
44
+ end
45
+ dispatch_instead(Tag.new(@last_timestamp, evt = Event.new(:onNextSegment, :file => ::File.basename(h.path))))
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,127 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+ class MetaDataMaker < Base
5
+ CHUNK_LENGTH_SIZE = 4 # todo: calc instead?
6
+ TAG_HEADER_SIZE = 11 # todo: calc instead?
7
+ TOTAL_EXTRA_SIZE_PER_TAG = CHUNK_LENGTH_SIZE + TAG_HEADER_SIZE
8
+ Info = Struct.new(:bytes, :tag_count, :first, :last)
9
+
10
+ def total_size
11
+ @info.values.sum{|h| h.bytes + h.tag_count * TOTAL_EXTRA_SIZE_PER_TAG}
12
+ end
13
+
14
+ def on_header(header)
15
+ @cue_points = []
16
+ @key_frames = []
17
+ @video_interval_stats = Hash.new(0)
18
+ @info = Hash.new{|h, key| h[key] = Info.new(0,0,nil,nil)}
19
+ @info[Header].bytes = header.size
20
+ # Header is not a tag, so leave tag_count at 0
21
+ end
22
+
23
+ def on_tag(tag)
24
+ h = @info[tag.body.class]
25
+ h.first ||= tag
26
+ h.last = tag
27
+ h.bytes += tag.body.size
28
+ h.tag_count += 1
29
+ end
30
+
31
+ def on_meta_data(t)
32
+ @previous_meta = t
33
+ absorb
34
+ end
35
+
36
+ def on_video(t)
37
+ @video_interval_stats[t.timestamp.in_milliseconds - @info[Video].last.timestamp.in_milliseconds] += 1 if @info[Video].last
38
+ end
39
+
40
+ def on_keyframe(t)
41
+ @key_frames << [t.timestamp.in_seconds, self.total_size]
42
+ end
43
+
44
+ def on_cue_point(t)
45
+ @cue_points << t
46
+ end
47
+
48
+ def time_positions_to_hash(time_position_pairs)
49
+ times, filepositions = time_position_pairs.transpose
50
+ {:times => times || [], :filepositions => filepositions || []}
51
+ end
52
+
53
+ def meta_data
54
+ frame_sequence_in_ms = @video_interval_stats.index(@video_interval_stats.values.max)
55
+ last_ts = @info.values.map{|h| h.last}.compact.map(&:timestamp).map(&:in_seconds).max
56
+ duration = last_ts + frame_sequence_in_ms/1000.0
57
+
58
+ meta = @info[Video].last.body.dimensions if @info[Video].last
59
+ meta ||= {:width => @previous_meta[:width], :height => @previous_meta[:height]} if @previous_meta
60
+ meta ||= {}
61
+ [Video, Audio].each do |k|
62
+ h = @info[k]
63
+ type = k.name.gsub!("FLV::","").downcase
64
+
65
+ meta[:"#{type}size"] = h.bytes + h.tag_count * TAG_HEADER_SIZE
66
+ meta[:"#{type}datarate"] = h.bytes / duration * 8 / 1000 #kbits/s
67
+ if meta[:"has#{type.capitalize}"] = h.first != nil
68
+ meta[:"#{type}codecid"] = h.first.codec_id
69
+ end
70
+ end
71
+
72
+ meta.merge!(
73
+ :filesize => total_size,
74
+ :datasize => total_size - @info[Header].bytes - CHUNK_LENGTH_SIZE*(1 + @info.values.sum(&:tag_count)),
75
+ :lasttimestamp => last_ts,
76
+ :framerate => frame_sequence_in_ms ? 1000.0/frame_sequence_in_ms : 0,
77
+ :duration => duration,
78
+
79
+ :hasCuePoints => !@cue_points.empty?,
80
+ :cuePoints => @cue_points.map(&:body),
81
+ :hasKeyframes => !@key_frames.empty?,
82
+ :keyframes => time_positions_to_hash(@key_frames),
83
+
84
+ :hasMetadata => true, #duh! Left for compatibility with FLVTool2, but I can't believe anyone would actually check on this...
85
+ :metadatadate => Time.now,
86
+ :metadatacreator => 'flvedit........................................................'
87
+ )
88
+
89
+ meta.merge!(
90
+ :audiodelay => @info[Video].first.timestamp.in_seconds,
91
+ :canSeekToEnd => @info[Video].last.frame_type == :keyframe,
92
+ :lastkeyframetimestamp => @key_frames.last.first || 0
93
+ ) if meta[:hasVideo]
94
+
95
+ meta.merge!(
96
+ :stereo => @info[Audio].first.channel == :stereo,
97
+ :audiosamplerate => @info[Audio].first.rate,
98
+ :audiosamplesize => @info[Audio].first.sample_size
99
+ ) if meta[:hasAudio]
100
+
101
+ # Adjust all sizes for this new meta tag
102
+ meta_size = Tag.new(0, meta).size
103
+ # p "Size of meta: #{meta_size}"
104
+ # p "Info: #{@info.values.map(&:bytes).inspect} #{@info.values.map(&:tag_count).inspect}"
105
+ meta[:filesize] += meta_size
106
+ meta[:datasize] += meta_size
107
+ meta[:keyframes][:filepositions].map! {|ts| ts + meta_size} if meta[:hasKeyframes]
108
+ meta[:cuePoints][:filepositions].map! {|ts| ts + meta_size} if meta[:hasCuePoints]
109
+
110
+ meta
111
+ end
112
+
113
+ # def add_meta_data_tag(stream, options)
114
+ # # add onLastSecond tag
115
+ # onlastsecond = FLV::FLVMetaTag.new
116
+ # onlastsecond.timestamp = ((stream.duration - 1) * 1000).to_int
117
+ # stream.add_tags(onlastsecond, false) if onlastsecond.timestamp >= 0
118
+ #
119
+ # stream.add_meta_tag({ 'metadatacreator' => options[:metadatacreator], 'metadatadate' => Time.now }.merge(options[:metadata]))
120
+ # unless options[:compatibility_mode]
121
+ # stream.on_meta_data_tag.meta_data['duration'] += (stream.frame_sequence_in_ms || 0) / 1000.0
122
+ # end
123
+ # end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "printer"
2
+ module FLV
3
+ module Edit
4
+ module Processor
5
+ class Print < Base
6
+ desc "Prints out meta data to stdout"
7
+ def on_meta_data(tag)
8
+ tag.debug(Printer.new(stdout))
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+
5
+ class Printer
6
+ def initialize(io, options={})
7
+ @io = io
8
+ @options = {:width => 50, :column_width => 15, :separator => "| "}.merge(options)
9
+ @margin_left = ""
10
+ end
11
+
12
+ def header(left, right)
13
+ @io.puts left + @options[:separator] + right
14
+ @margin_left = " "*left.length + @options[:separator]
15
+ end
16
+
17
+ # Prints out a hash (or any list of key-value pairs) in two columns
18
+ def values(hash)
19
+ hash.map{|k,v| [k.to_s, v] }.sort.each do |key, value|
20
+ @io.puts "#{@margin_left}#{key.to_s.ljust(@options[:column_width])}: #{value.inspect.delete(':"')}"
21
+ end
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+ class Reader < Base
5
+ def initialize(*)
6
+ super
7
+ rewind
8
+ raise "Oups, Filenames were #{@options[:files].inspect}" if @options[:files].include? nil
9
+ raise "Please specify at least one filename" if @options[:files].empty?
10
+ end
11
+
12
+ def has_next_file?
13
+ @to_process > 0
14
+ end
15
+
16
+ def rewind
17
+ @to_process = @options[:files].length
18
+ end
19
+
20
+ def process_next_file
21
+ raise IndexError, "No more filenames to process" unless has_next_file?
22
+ @to_process -= 1
23
+ FLV::File.open(@options[:files][-1- @to_process]) do |f|
24
+ dispatch_chunks(f)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+ class Save < Base
5
+ desc "Saves the result to PATH", :param => {:class => String, :name => "PATH"}
6
+ def process_next_file
7
+ super
8
+ ensure
9
+ if @out
10
+ @out.close
11
+ finalpath = @out.path.sub(/\.temp$/, '')
12
+ FileUtils.mv(@out.path, finalpath) unless finalpath == @out.path
13
+ end
14
+ end
15
+
16
+ def on_header(h)
17
+ @out = FLV::File::open(options[:save] || (h.path+".temp"), "w+b")
18
+ @out << h
19
+ end
20
+
21
+ def on_tag(t)
22
+ @out << t
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "meta_data_maker"
2
+
3
+ module FLV
4
+ module Edit
5
+ module Processor
6
+ class Update < Base
7
+ desc "Updates FLV with an onMetaTag event"
8
+ def initialize(source=nil, options={})
9
+ super
10
+ @meta_data_maker = MetaDataMaker.new(source.dup, options)
11
+ end
12
+
13
+ def each(&block)
14
+ @meta_data_maker.each {}
15
+ ensure # even if each throws, we better call super otherwise we won't be synchronized anymore!
16
+ super
17
+ end
18
+
19
+ absorb :on_meta_data, :on_last_second
20
+
21
+ def on_header(header)
22
+ dispatch_instead header, Tag.new(0, @meta_data_maker.meta_data)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ %w(base add command_line cut debug head join print reader save update).each do |proc|
2
+ require_relative "processor/#{proc}"
3
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'options'
2
+ require_relative 'processor'
3
+
4
+ module FLV
5
+ module Edit
6
+ class Runner
7
+ attr_reader :options, :commands
8
+
9
+ def initialize(*arg)
10
+ @commands, @options = (arg.length == 1 ? Options.new(arg.first).to_a : arg)
11
+ end
12
+
13
+ def run
14
+ commands = [*@commands].map{|c| c.is_a?(Class) ? c : Processor.const_get(c.to_s.camelize)}
15
+ commands.unshift Processor::Reader
16
+ commands << Processor::CommandLine unless options[:dont_catch_errors]
17
+ Processor.chain(commands, @options).process_all
18
+ end
19
+
20
+ alias_method :run!, :run
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module FLV
2
+ module Edit
3
+ FILE = ::File.dirname(__FILE__) + '/../../../VERSION.yml'
4
+
5
+ class Version < Struct.new(:major, :minor, :patch)
6
+ def to_s
7
+ "#{major}.#{minor}.#{patch}"
8
+ end
9
+ end
10
+
11
+ def self.version
12
+ Version.new(*YAML.load_file(FILE).values_at('major','minor','patch'))
13
+ end
14
+ end
15
+ end
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