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 +137 -0
- data/VERSION.yml +4 -0
- data/bin/flvedit +14 -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/options.rb +162 -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/processor.rb +3 -0
- data/lib/flv/edit/runner.rb +23 -0
- data/lib/flv/edit/version.rb +15 -0
- data/lib/flv/edit.rb +20 -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/lib/flv.rb +24 -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
data/lib/flv/event.rb
ADDED
@@ -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
|
data/lib/flv/file.rb
ADDED
@@ -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
|
data/lib/flv/header.rb
ADDED
@@ -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
|
data/lib/flv/packing.rb
ADDED
@@ -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
|
data/lib/flv/tag.rb
ADDED
@@ -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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FLV
|
2
|
+
class IOError < ::IOError # :nodoc:
|
3
|
+
end
|
4
|
+
|
5
|
+
module Util # :nodoc:
|
6
|
+
def self.double_check(event, expected, actual)
|
7
|
+
Checking.fail_check(event, expected, actual) unless [*expected].include? actual
|
8
|
+
end
|
9
|
+
|
10
|
+
class Checking # :nodoc:
|
11
|
+
class << self
|
12
|
+
attr_accessor :strict
|
13
|
+
def fail_check(event, expected, actual)
|
14
|
+
err = "Mismatch on #{event}: expected #{expected} vs #{actual}"
|
15
|
+
raise IOError, err if strict
|
16
|
+
#STDERR << "Caution: "+ err
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/lib/flv/video.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module FLV
|
2
|
+
# The body of a video tag.
|
3
|
+
# The data is quite complex stuff. We make no attempt to understand it all
|
4
|
+
# or to be able to modify any of it. We simply consider it a complex string
|
5
|
+
# and read the interesting bits to give more info.
|
6
|
+
class Video < String
|
7
|
+
include Body
|
8
|
+
CODECS = Hash.new(:unknown).merge!(
|
9
|
+
1 => :JPEG,
|
10
|
+
2 => :h263,
|
11
|
+
3 => :screen,
|
12
|
+
4 => :on2_vp6,
|
13
|
+
5 => :on2_vp6_alpha,
|
14
|
+
6 => :screen_v2,
|
15
|
+
7 => :AVC
|
16
|
+
).freeze
|
17
|
+
|
18
|
+
FRAMES = Hash.new(:unknown).merge!(
|
19
|
+
1 => :keyframe ,
|
20
|
+
2 => :interframe,
|
21
|
+
3 => :disposable_interframe
|
22
|
+
).freeze
|
23
|
+
|
24
|
+
def frame_type
|
25
|
+
FRAMES[read_bits(0...4)]
|
26
|
+
end
|
27
|
+
|
28
|
+
def codec_id
|
29
|
+
read_bits(4...8)
|
30
|
+
end
|
31
|
+
|
32
|
+
def codec
|
33
|
+
CODECS[codec_id]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Dimensions for h263 encoding; either as a bit range or the final value
|
37
|
+
H263_DIMENSIONS = {
|
38
|
+
0 => [41...49, 49...57],
|
39
|
+
1 => [41...57, 57...73],
|
40
|
+
2 => [352, 288],
|
41
|
+
3 => [176, 144],
|
42
|
+
4 => [128, 96] ,
|
43
|
+
5 => [320, 240],
|
44
|
+
6 => [160, 120]
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
# Returns dimensions as {:width => w, :height => h}, for Sorensen H.263 and screen video codecs only (otherwise returns nil)
|
48
|
+
def dimensions
|
49
|
+
w, h = case codec
|
50
|
+
when :h263
|
51
|
+
H263_DIMENSIONS[read_bits(38...41)]
|
52
|
+
when :screen
|
53
|
+
[12...24, 24...32]
|
54
|
+
end
|
55
|
+
return nil unless w
|
56
|
+
w, h = [w, h].map{ |r| read_bits(r) } if w.is_a?(Range)
|
57
|
+
{:width => w, :height => h}
|
58
|
+
end
|
59
|
+
|
60
|
+
def is?(what)
|
61
|
+
frame_type.to_s.downcase == what.to_s.downcase || super
|
62
|
+
end
|
63
|
+
|
64
|
+
def getters
|
65
|
+
super - [:frame_type, :frame_type.to_s] # Let's exclude the frame_type from the normal attributes... (string vs symbol: ruby 1.8 vs 1.9)
|
66
|
+
end
|
67
|
+
|
68
|
+
def title
|
69
|
+
super + " (#{frame_type})" # ...and include it in the title instead.
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/lib/flv.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'backports'
|
3
|
+
|
4
|
+
# utilities
|
5
|
+
require_relative 'flv/util/double_check'
|
6
|
+
|
7
|
+
# packing of FLV objects:
|
8
|
+
require 'packable'
|
9
|
+
require_relative 'flv/packing'
|
10
|
+
require_relative 'flv/base'
|
11
|
+
|
12
|
+
# FLV body of tags
|
13
|
+
require_relative 'flv/body'
|
14
|
+
require_relative 'flv/audio'
|
15
|
+
require_relative 'flv/video'
|
16
|
+
require_relative 'flv/event'
|
17
|
+
|
18
|
+
# FLV chunks (tags & header)
|
19
|
+
require_relative 'flv/timestamp'
|
20
|
+
require_relative 'flv/tag'
|
21
|
+
require_relative 'flv/header'
|
22
|
+
|
23
|
+
# finally:
|
24
|
+
require_relative 'flv/file'
|
Binary file
|
Binary file
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<tags>
|
3
|
+
<!-- an event cue point -->
|
4
|
+
<metatag event="onCuePoint">
|
5
|
+
<name>TestEvent1</name>
|
6
|
+
<timestamp>333</timestamp>
|
7
|
+
<parameters>
|
8
|
+
<speaker>Peter</speaker>
|
9
|
+
<says>Hello my Name is Peter.</says>
|
10
|
+
</parameters>
|
11
|
+
<type>event</type>
|
12
|
+
</metatag>
|
13
|
+
|
14
|
+
<!-- a navigation cue point -->
|
15
|
+
<metatag event="onCuePoint">
|
16
|
+
<name>TestEvent2</name>
|
17
|
+
<timestamp>1000</timestamp>
|
18
|
+
<parameters>
|
19
|
+
<index>1</index>
|
20
|
+
<title>Chapter 1</title>
|
21
|
+
</parameters>
|
22
|
+
<type>navigation</type>
|
23
|
+
</metatag>
|
24
|
+
|
25
|
+
<!-- this cuepoint overwrites the previous cue point, because of it's
|
26
|
+
identical timestamps and the overwrite parameter was set -->
|
27
|
+
|
28
|
+
<metatag event="onCuePoint" overwrite="true">
|
29
|
+
<name>TestEvent3</name>
|
30
|
+
<timestamp>1000</timestamp>
|
31
|
+
<parameters>
|
32
|
+
<index>1</index>
|
33
|
+
<title>Chapter 2</title>
|
34
|
+
</parameters>
|
35
|
+
<type>navigation</type>
|
36
|
+
</metatag>
|
37
|
+
</tags>
|
38
|
+
|
39
|
+
|