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