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
@@ -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,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,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
|