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