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