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.
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