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,40 @@
1
+ module FLV
2
+ # The body of a tag containing meta data, cue points or last second information
3
+ # These behave like a Hash. The keys should be symbols
4
+ # while the values can be about any type, including arrays and hashes.
5
+ class Event < Hash
6
+ include Packable
7
+ include Body
8
+ TYPICAL_EVENTS = [:onMetaData, :onCuePoint, :onCaption, :onCaptionInfo, :onLastSecond, :onEvent]
9
+ attr_accessor :event
10
+
11
+ def initialize(event = :onMetaData, h = {})
12
+ self.replace h
13
+ self.event = event.to_sym
14
+ end
15
+
16
+ def read_packed(io,options) #:nodoc:
17
+ len = io.pos_change do
18
+ evt, h = io >>:flv_value >>:flv_value
19
+ self.event = evt.to_sym
20
+ replace h
21
+ end
22
+ FLV::Util.double_check :size, options[:bytes], len
23
+ end
24
+
25
+ def write_packed(io,*) #:nodoc:
26
+ io << [event.to_s, :flv_value] << [self, :flv_value]
27
+ end
28
+
29
+ def debug(format, *)
30
+ format.values(:event => event)
31
+ format.values(self)
32
+ end
33
+
34
+ def is?(what)
35
+ event.to_s == what.to_s || super
36
+ end
37
+
38
+ alias_method :similar_to?, :==
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ module FLV
2
+ module File
3
+ def each(*arg, &block)
4
+ return super unless arg.empty?
5
+ return to_enum unless block_given?
6
+ h= read(Header)
7
+ yield h
8
+ super(Tag, &block)
9
+ end
10
+
11
+ def self.open(*arg)
12
+ file = ::File.open(*arg)
13
+ begin
14
+ file = return_value = file.packed.extend(File)
15
+ rescue Exception
16
+ file.close
17
+ raise
18
+ end
19
+ begin
20
+ return_value = yield(file)
21
+ ensure
22
+ file.close
23
+ end if block_given?
24
+ return_value
25
+ end
26
+
27
+ def self.read(portname, *arg)
28
+ open(portname) do |f|
29
+ return f.to_a if arg.empty?
30
+ n, offset = arg.first, arg[1] || 0
31
+ f.each.first(n+offset)[offset, offset+n-1]
32
+ end
33
+ end
34
+
35
+ def self.foreach(portname, *, &block)
36
+ open(portname) do |f|
37
+ f.each(&block)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ module FLV
2
+ # Represents the header chunk present at the very beginning of any FLV file.
3
+ class Header
4
+ include Base
5
+ attr_accessor :version, :has_audio, :has_video, :extra, :path
6
+
7
+ FLV_SIGNATURE = [String, {:bytes => 3}].freeze
8
+ SIGNATURE = 'FLV'
9
+ MIN_OFFSET = 9
10
+ FLAGS = { :has_video => 1, :has_audio => 4 }.freeze
11
+
12
+ def read_packed(io, *) #:nodoc:
13
+ signature, self.version, type_flags, offset = \
14
+ io >> FLV_SIGNATURE >> :char >> :char >> :unsigned_long
15
+
16
+ raise RuntimeError.new("typeflags is #{signature}, #{version}, #{type_flags}, #{offset}") unless Fixnum === type_flags
17
+
18
+ FLAGS.each {|flag,mask| send("#{flag}=", type_flags & mask > 0) }
19
+ self.extra = io.read(offset - MIN_OFFSET)
20
+ ignore_PreviousTagSize0 = io.read :unsigned_long
21
+ self.path = io.try :path
22
+ raise IOError("Wrong Signature (#{signature} instead of #{SIGNATURE})") unless SIGNATURE == signature
23
+ end
24
+
25
+ def write_packed(io, *) #:nodoc:
26
+ self.extra ||= ""
27
+ type_flags = FLAGS.sum{|flag, mask | send(flag) ? mask : 0}
28
+ io << SIGNATURE << [self.version || 1, :char] << [type_flags, :char] <<
29
+ [MIN_OFFSET + self.extra.length, :unsigned_long] << self.extra << [0, :unsigned_long]
30
+ end
31
+
32
+ def debug(format, *)
33
+ format.header("Header", path)
34
+ format.values(to_h.tap{|h| h.delete(:path)})
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,140 @@
1
+ require 'singleton'
2
+
3
+ module FLV
4
+ class Event < Hash ; end
5
+ # FLV files can contain structured data. This modules makes it easy to (un)pack that data.
6
+ # The packing option +flv_value+ can (un)pack any kind of variable.
7
+ # It corresponds to +SCRIPTDATAVALUE+ in the official FLV file format spec.
8
+ # Implementation Note:
9
+ # +flv_value+ is actually simply a wrapper that will (un)pack the type of variable;
10
+ # the actual data is (un)packed with the format +flv+, which is independently defined for each type
11
+ module Packing
12
+
13
+ # A special kind of value used to signify the end of a list (implemented at the end)
14
+ class EndOfList # :nodoc:
15
+ end
16
+
17
+ #=== Top-level :flv_value filter:
18
+
19
+ TYPE_TO_CLASS = Hash.new do |h, type|
20
+ #EndOfList
21
+ raise IOError, "Invalid type for a flash variable. #{type.inspect} is not in #{h.keys.sort.inspect}" #todo: handle error for corrupted da
22
+ end.merge!(
23
+ 0 => Numeric ,
24
+ 1 => TrueClass , # There really should be a Boolean class!
25
+ 2 => String ,
26
+ 3 => Hash ,
27
+ 8 => [Hash, :flv_with_size],
28
+ 9 => EndOfList ,
29
+ 10 => Array ,
30
+ 11 => Time
31
+ ).freeze
32
+
33
+ CLASS_TO_TYPE = Hash.new do |h, klass|
34
+ h[klass] = h[klass.superclass] # Makes it such that CLASS_TO_TYPE[Fixnum] = CLASS_TO_TYPE[Integer]
35
+ end.merge!(TYPE_TO_CLASS.invert).merge!(
36
+ Event => TYPE_TO_CLASS.key([Hash, :flv_with_size]), # Write Events as hashes with size
37
+ FalseClass => TYPE_TO_CLASS.key(TrueClass)
38
+ )
39
+
40
+ # Read/write the type and (un)pack the actual data with :flv
41
+ Object.packers.set(:flv_value) do |packer|
42
+ packer.write do |io|
43
+ type_nb = CLASS_TO_TYPE[self.class]
44
+ klass, format = TYPE_TO_CLASS[type_nb]
45
+ io << [type_nb, :char] << [self, format || :flv]
46
+ end
47
+ packer.read do |io|
48
+ klass, format = TYPE_TO_CLASS[io.read(:char)]
49
+ io.read([klass, format || :flv])
50
+ end
51
+ end
52
+
53
+ #=== For each basic type, the :flv filter (un)packs the actual data:
54
+
55
+ Numeric.packers.set(:flv) do |packer|
56
+ # Both Integers and Floats are packed as doubles
57
+ packer.write {|io| io << [to_f, :double] }
58
+ packer.read do |io|
59
+ n = io.read(:double)
60
+ n.to_i == n ? n.to_i : n
61
+ end
62
+ end
63
+
64
+ [TrueClass, FalseClass].each do |klass|
65
+ klass.packers.set(:flv) do |packer|
66
+ packer.write {|io| io << [self ? 1 : 0, :char] }
67
+ packer.read {|io| io.read(:char) != 0 }
68
+ end
69
+ end
70
+
71
+ String.packers.set(:flv) do |packer|
72
+ packer.write {|io| io << [length, :unsigned_short] << self }
73
+ packer.read {|io| io.read(io.read(:unsigned_short)) }
74
+ end
75
+
76
+ Array.packers.set(:flv) do |packer|
77
+ packer.write do |io|
78
+ io << [length, :unsigned_long]
79
+ each do |value|
80
+ io << [value, :flv_value]
81
+ end
82
+ end
83
+ packer.read do |io|
84
+ nb = io.read(:unsigned_long)
85
+ io.each(:flv_value).take(nb)
86
+ end
87
+ end
88
+
89
+ # The default format for hashes has a useless hint for the size
90
+ # This filter simply acts as a wrapper for the :flv_without_size filter
91
+ Hash.packers.set(:flv_with_size) do |packer|
92
+ packer.write do |io|
93
+ io << [length, :unsigned_long] << [self, :flv]
94
+ end
95
+ packer.read do |io|
96
+ ignore_length_hint = io >> :unsigned_long
97
+ io.read [Hash, :flv]
98
+ end
99
+ end
100
+
101
+ Hash.packers.set(:flv) do |packer|
102
+ packer.write do |io|
103
+ each do |key, value|
104
+ io << [key.to_s, :flv] << [value, :flv_value]
105
+ end
106
+ io << ["", :flv] << [EndOfList.instance, :flv_value]
107
+ end
108
+ packer.read do |io|
109
+ Hash[
110
+ io.each([String, :flv], :flv_value).
111
+ take_while {|str, val| val != EndOfList.instance }.
112
+ map{|k,v| [k.to_sym, v]}
113
+ ]
114
+ end
115
+ end
116
+
117
+ Time.packers.set(:flv) do |packer|
118
+ packer.write {|io| io << [to_f * 1000, :double] << [Time.now.gmtoff / 60, :short] }
119
+ packer.read do |io|
120
+ seconds, zone = io >> :double >> :short
121
+ Time.at((seconds / 1000).to_i) + (zone * 60) - Time.now.gmtoff
122
+ end
123
+ end
124
+
125
+ class EndOfList # :nodoc:
126
+ include Singleton, Packable
127
+ packers.set :flv, {}
128
+
129
+ def write_packed(*)
130
+ # no data to write
131
+ end
132
+
133
+ def self.read_packed(*)
134
+ self.instance
135
+ end
136
+ end
137
+
138
+ Integer.packers.set :unsigned_24bits, :bytes => 3, :signed => false, :endian => :big
139
+ end #module Packing
140
+ end #module FLV
@@ -0,0 +1,62 @@
1
+ module FLV
2
+ # FLV files consists of a single header and a collection of Tags.
3
+ # Tags all have a timestamp and a body; this body can be Audio, Video, or Event.
4
+ class Tag
5
+ include Base
6
+ attr_accessor :body, :timestamp
7
+
8
+ def timestamp=(t)
9
+ @timestamp = Timestamp.try_convert(t)
10
+ end
11
+
12
+ def initialize(timestamp = 0, body = nil)
13
+ self.body = body.instance_of?(Hash) ? Event.new(:onMetaData, body) : body
14
+ self.timestamp = timestamp
15
+ end
16
+
17
+ CLASS_CODE = {
18
+ 8 => Audio,
19
+ 9 => Video,
20
+ 18 => Event
21
+ }.freeze
22
+
23
+ def write_packed(io, *) #:nodoc
24
+ packed_body = @body.pack
25
+ len = io.pos_change do
26
+ io << [CLASS_CODE.key(self.body.class), :char] \
27
+ << [packed_body.length, :unsigned_24bits] \
28
+ << [@timestamp.in_milliseconds, :unsigned_24bits] \
29
+ << [@timestamp.in_milliseconds >>24, :char] \
30
+ << [streamid=0, :unsigned_24bits] \
31
+ << packed_body
32
+ end
33
+ io << [len, :unsigned_long]
34
+ end
35
+
36
+ def read_packed(io, options) #:nodoc
37
+ len = io.pos_change do
38
+ code, body_len, timestamp_in_ms, timestamp_in_ms_ext, streamid =
39
+ io >>:char >>:unsigned_24bits >>:unsigned_24bits >>:char >>:unsigned_24bits
40
+ @timestamp = Timestamp.in_milliseconds(timestamp_in_ms + (timestamp_in_ms_ext << 24))
41
+ @body = io.read CLASS_CODE[code] || Body, :bytes => body_len
42
+ end
43
+ FLV::Util.double_check :size, len, io.read(:unsigned_long) #todo
44
+ end
45
+
46
+ def debug(format, compared_with = nil) #:nodoc
47
+ format.header("#{timestamp} ", @body.title)
48
+ @body.debug(format) unless compared_with && @body.similar_to?(compared_with.body)
49
+ end
50
+
51
+ def is?(what) #:nodoc
52
+ super || body.is?(what)
53
+ end
54
+
55
+ def method_missing(*arg, &block)
56
+ super
57
+ rescue NoMethodError
58
+ body.send(*arg, &block)
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,124 @@
1
+ require 'delegate'
2
+ require 'optparse'
3
+
4
+ module FLV
5
+ class Timestamp < DelegateClass(Float)
6
+ def initialize(ms=0)
7
+ raise ArgumentError unless ms.is_a? Numeric
8
+ super
9
+ end
10
+
11
+ # Returns [hours, minutes, seconds, milliseconds]
12
+ def to_a
13
+ n = in_milliseconds
14
+ [1000,60,60].map do |div|
15
+ val = n % div
16
+ n /= div
17
+ val
18
+ end.reverse.unshift(n)
19
+ end
20
+
21
+ # Returns "HH:MM:SS.MMMM" like "1:23:45.678" or "1:00.000" for 1 minute.
22
+ def to_s
23
+ return "" if self == INFINITY
24
+ nbs = to_a.drop_while(&:zero?)
25
+ ms = nbs.pop || 0
26
+ first = nbs.shift || 0
27
+ sprintf("%d%s.%03d", first, nbs.map{|n| sprintf(":%02d",n)}.join, ms)
28
+ end
29
+
30
+ def in_seconds
31
+ to_f
32
+ end
33
+
34
+ def in_milliseconds
35
+ (self * 1000).round
36
+ end
37
+
38
+ def widen(amount)
39
+ TimestampRange.new(self,self).widen(amount)
40
+ end
41
+
42
+ def self.in_milliseconds(ms)
43
+ Timestamp.new(ms/1000.0)
44
+ end
45
+
46
+ def self.in_seconds(s)
47
+ new s
48
+ end
49
+
50
+ def self.try_convert(s, if_empty_string = 0)
51
+ case s
52
+ when Timestamp
53
+ s
54
+ when Numeric
55
+ new s
56
+ when ""
57
+ new if_empty_string
58
+ when REGEXP
59
+ h, m, s, ms = Regexp.last_match.captures
60
+ ms &&= ms.ljust(3,'0')[0,3] # e.g. 1.23 => 1.230
61
+ h, m = m, h if h and h.end_with?(":") and m.nil?
62
+ h, m, s, ms = [h, m, s, ms].map{|n| (n || 0).to_i}
63
+ in_seconds ((h*60+m)*60+s)+ms/1000.0
64
+ end
65
+ end
66
+
67
+ REGEXP = /^(\d*[h:])?(\d*[m:])?(\d*)\.?(\d*)$/.freeze
68
+ end # Timestamp
69
+
70
+ INFINITY = 1/0.0
71
+ class TimestampRange < Range
72
+ core = Timestamp::REGEXP.source.gsub(/[\$\^\(\)]/,"")
73
+ REGEXP = Regexp.new("^(#{core})-(#{core})$").freeze
74
+
75
+ def to_s
76
+ "#{self.begin}-#{self.end}"
77
+ end
78
+
79
+ def initialize(from, to, exclusive=false)
80
+ super(Timestamp.try_convert(from), Timestamp.try_convert(to, INFINITY), exclusive)
81
+ end
82
+
83
+ def in_seconds
84
+ Range.new(self.begin.in_seconds, self.end.in_seconds, self.exclude_end?)
85
+ end
86
+
87
+ def in_milliseconds
88
+ Range.new(self.begin.in_milliseconds, self.end.in_milliseconds, self.exclude_end?)
89
+ end
90
+
91
+ def self.try_convert(s)
92
+ case s
93
+ when Range
94
+ new s.begin, s.end
95
+ when TimestampRange
96
+ s
97
+ when REGEXP
98
+ new *Regexp.last_match.captures
99
+ else
100
+ p "Can't convert #{s}"
101
+ end
102
+ end
103
+
104
+ def widen(amount)
105
+ TimestampRange.new [self.begin - amount, 0].max, self.end + amount, self.exclude_end?
106
+ end
107
+ end # TimestampRange
108
+ OptionParser.accept(TimestampRange, TimestampRange::REGEXP) {|str,from,to| TimestampRange.try_convert(str)}
109
+
110
+
111
+ class TimestampOrTimestampRange # :nodoc:
112
+ core = Timestamp::REGEXP.source.gsub(/[\$\^\(\)]/,"")
113
+ REGEXP = Regexp.new("^(#{core})-(#{core})|(#{core})$").freeze
114
+ end # TimestampOrTimestampRange
115
+ OptionParser.accept(TimestampOrTimestampRange, TimestampOrTimestampRange::REGEXP) do |str,from, to, from_to|
116
+ (from_to ? Timestamp : TimestampRange).try_convert(str)
117
+ end
118
+ end
119
+
120
+ class Range # :nodoc:
121
+ def ==(r) # Override built-in == because of bug in Ruby 1.8 & 1.9, see http://redmine.ruby-lang.org/issues/show/1165
122
+ self.begin == r.begin && self.end == r.end && self.exclude_end? == r.exclude_end?
123
+ end
124
+ end