marcandre-flvedit 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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 +131 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/flvedit +14 -0
  7. data/lib/flv.rb +24 -0
  8. data/lib/flv/audio.rb +66 -0
  9. data/lib/flv/base.rb +38 -0
  10. data/lib/flv/body.rb +57 -0
  11. data/lib/flv/edit.rb +20 -0
  12. data/lib/flv/edit/options.rb +162 -0
  13. data/lib/flv/edit/processor.rb +3 -0
  14. data/lib/flv/edit/processor/add.rb +67 -0
  15. data/lib/flv/edit/processor/base.rb +209 -0
  16. data/lib/flv/edit/processor/command_line.rb +23 -0
  17. data/lib/flv/edit/processor/cut.rb +27 -0
  18. data/lib/flv/edit/processor/debug.rb +30 -0
  19. data/lib/flv/edit/processor/head.rb +16 -0
  20. data/lib/flv/edit/processor/join.rb +52 -0
  21. data/lib/flv/edit/processor/meta_data_maker.rb +127 -0
  22. data/lib/flv/edit/processor/print.rb +13 -0
  23. data/lib/flv/edit/processor/printer.rb +27 -0
  24. data/lib/flv/edit/processor/reader.rb +30 -0
  25. data/lib/flv/edit/processor/save.rb +28 -0
  26. data/lib/flv/edit/processor/update.rb +27 -0
  27. data/lib/flv/edit/runner.rb +23 -0
  28. data/lib/flv/edit/version.rb +15 -0
  29. data/lib/flv/event.rb +40 -0
  30. data/lib/flv/file.rb +41 -0
  31. data/lib/flv/header.rb +37 -0
  32. data/lib/flv/packing.rb +140 -0
  33. data/lib/flv/tag.rb +62 -0
  34. data/lib/flv/timestamp.rb +124 -0
  35. data/lib/flv/util/double_check.rb +22 -0
  36. data/lib/flv/video.rb +73 -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,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,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