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