flvtool2 1.0.6
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 +109 -0
- data/LICENSE +24 -0
- data/README +27 -0
- data/Rakefile +76 -0
- data/bin/flvtool2 +3 -0
- data/examples/tags.xml +39 -0
- data/flvtool2.exy +22 -0
- data/lib/flv.rb +1 -0
- data/lib/flv/amf_string_buffer.rb +280 -0
- data/lib/flv/audio_tag.rb +93 -0
- data/lib/flv/core_extensions.rb +141 -0
- data/lib/flv/meta_tag.rb +78 -0
- data/lib/flv/stream.rb +492 -0
- data/lib/flv/tag.rb +120 -0
- data/lib/flv/video_tag.rb +114 -0
- data/lib/flvtool2.rb +230 -0
- data/lib/flvtool2/base.rb +297 -0
- data/lib/flvtool2/version.rb +10 -0
- data/lib/mixml.rb +119 -0
- data/lib/miyaml.rb +63 -0
- data/setup.rb +1585 -0
- metadata +70 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
# Copyright (c) 2005 Norman Timmler (inlet media e.K., Hamburg, Germany)
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
# 1. Redistributions of source code must retain the above copyright
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer in the
|
11
|
+
# documentation and/or other materials provided with the distribution.
|
12
|
+
# 3. The name of the author may not be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
18
|
+
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
19
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
20
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
21
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
22
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
24
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
|
26
|
+
|
27
|
+
module FLV
|
28
|
+
|
29
|
+
class FLVAudioTag < FLVTag
|
30
|
+
|
31
|
+
UNCOMPRESSED = 0
|
32
|
+
ADPCM = 1
|
33
|
+
MP3 = 2
|
34
|
+
NELLYMOSER8KHZMONO = 5
|
35
|
+
NELLYMOSER = 6
|
36
|
+
|
37
|
+
MONO = 0
|
38
|
+
STEREO = 1
|
39
|
+
|
40
|
+
attr_reader :sound_format,
|
41
|
+
:sound_rate,
|
42
|
+
:sound_sample_size,
|
43
|
+
:sound_type
|
44
|
+
|
45
|
+
def after_initialize(new_object)
|
46
|
+
@tag_type = AUDIO
|
47
|
+
read_header
|
48
|
+
end
|
49
|
+
|
50
|
+
def name
|
51
|
+
'Audio Tag'
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_header
|
55
|
+
data_stream = AMFStringBuffer.new(@data)
|
56
|
+
bit_sequence = data_stream.read__STRING(1).unpack('B8').to_s
|
57
|
+
|
58
|
+
@sound_format = bit2uint(bit_sequence[0,4])
|
59
|
+
@sound_rate = case bit2uint(bit_sequence[4,2])
|
60
|
+
when 0
|
61
|
+
5500
|
62
|
+
when 1
|
63
|
+
11000
|
64
|
+
when 2
|
65
|
+
22000
|
66
|
+
when 3
|
67
|
+
44000
|
68
|
+
end
|
69
|
+
@sound_sample_size = case bit2uint(bit_sequence[6,1])
|
70
|
+
when 0
|
71
|
+
8
|
72
|
+
when 1
|
73
|
+
16
|
74
|
+
end
|
75
|
+
@sound_type = bit2uint(bit_sequence[7,1])
|
76
|
+
|
77
|
+
# Nellymoser 8kHz mono special case
|
78
|
+
if @sound_format == NELLYMOSER8KHZMONO
|
79
|
+
@sound_rate = 8000
|
80
|
+
@sound_type = MONO
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def inspect
|
85
|
+
out = super
|
86
|
+
out << "sound_format: #{['Uncompressed', 'ADPCM', 'MP3', nil, nil, 'Nellymoser 8KHz mono', 'Nellymoser'][@sound_format]}"
|
87
|
+
out << "sound_rate: #{@sound_rate}"
|
88
|
+
out << "sound_sample_size: #{@sound_sample_size}"
|
89
|
+
out << "sound_type: #{['Mono', 'Stereo'][@sound_type]}"
|
90
|
+
out
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# Copyright (c) 2005 Norman Timmler (inlet media e.K., Hamburg, Germany)
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
# 1. Redistributions of source code must retain the above copyright
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer in the
|
11
|
+
# documentation and/or other materials provided with the distribution.
|
12
|
+
# 3. The name of the author may not be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
18
|
+
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
19
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
20
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
21
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
22
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
24
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
|
26
|
+
|
27
|
+
class Time
|
28
|
+
alias :to_str :to_s
|
29
|
+
DAY_NAME = [
|
30
|
+
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
|
31
|
+
]
|
32
|
+
MONTH_NAME = [
|
33
|
+
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
34
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
35
|
+
]
|
36
|
+
def to_s
|
37
|
+
sprintf('%s %s %d %02d:%02d:%02d GMT',
|
38
|
+
DAY_NAME[wday],
|
39
|
+
MONTH_NAME[mon-1], day,
|
40
|
+
hour, min, sec, year) +
|
41
|
+
(
|
42
|
+
off = Time.now.gmtoff
|
43
|
+
sign = off < 0 ? '-' : '+'
|
44
|
+
sprintf('%s%02d%02d', sign, *(off.abs / 60).divmod(60))
|
45
|
+
) +
|
46
|
+
(
|
47
|
+
sprintf(' %d', year)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
def to_iso
|
51
|
+
offset = Time.now.gmtoff
|
52
|
+
strftime("%Y-%m-%dT%H:%m:%S#{sprintf('%s%02d:%02d', (offset < 0 ? '-' : '+'), *(offset.abs / 60).divmod(60))}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Float
|
57
|
+
alias :to_str :to_s
|
58
|
+
def to_s
|
59
|
+
to_f % 1 == 0 ? to_i.to_s : to_str
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class IO
|
64
|
+
def read__UI8(position = nil)
|
65
|
+
seek position unless position.nil?
|
66
|
+
readchar
|
67
|
+
end
|
68
|
+
|
69
|
+
def read__UI16(position = nil)
|
70
|
+
seek position unless position.nil?
|
71
|
+
(readchar << 8) + readchar
|
72
|
+
end
|
73
|
+
|
74
|
+
def read__UI24(position = nil)
|
75
|
+
seek position unless position.nil?
|
76
|
+
(readchar << 16) + (readchar << 8) + readchar
|
77
|
+
end
|
78
|
+
|
79
|
+
def read__UI32(position = nil)
|
80
|
+
seek position unless position.nil?
|
81
|
+
(readchar << 24) + (readchar << 16) + (readchar << 8) + readchar
|
82
|
+
end
|
83
|
+
|
84
|
+
def read__STRING(length, position = nil)
|
85
|
+
seek position unless position.nil?
|
86
|
+
read length
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def write__UI8(value, position = nil)
|
91
|
+
seek position unless position.nil?
|
92
|
+
write [value].pack('C')
|
93
|
+
end
|
94
|
+
|
95
|
+
def write__UI24(value, position = nil)
|
96
|
+
seek position unless position.nil?
|
97
|
+
write [value >> 16].pack('c')
|
98
|
+
write [(value >> 8) & 0xff].pack('c')
|
99
|
+
write [value & 0xff].pack('c')
|
100
|
+
end
|
101
|
+
|
102
|
+
def write__UI32(value, position = nil)
|
103
|
+
seek position unless position.nil?
|
104
|
+
write [value].pack('N')
|
105
|
+
end
|
106
|
+
|
107
|
+
def write__STRING(string, position = nil)
|
108
|
+
seek position unless position.nil?
|
109
|
+
write string
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class ARGFWrapper
|
114
|
+
def readchar
|
115
|
+
ARGF.readchar
|
116
|
+
end
|
117
|
+
|
118
|
+
def read(length)
|
119
|
+
ARGF.read(length)
|
120
|
+
end
|
121
|
+
|
122
|
+
def read__UI8
|
123
|
+
readchar
|
124
|
+
end
|
125
|
+
|
126
|
+
def read__UI16
|
127
|
+
(readchar << 8) + readchar
|
128
|
+
end
|
129
|
+
|
130
|
+
def read__UI24
|
131
|
+
(readchar << 16) + (readchar << 8) + readchar
|
132
|
+
end
|
133
|
+
|
134
|
+
def read__UI32
|
135
|
+
(readchar << 24) + (readchar << 16) + (readchar << 8) + readchar
|
136
|
+
end
|
137
|
+
|
138
|
+
def read__STRING(length)
|
139
|
+
read length
|
140
|
+
end
|
141
|
+
end
|
data/lib/flv/meta_tag.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Copyright (c) 2005 Norman Timmler (inlet media e.K., Hamburg, Germany)
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
# 1. Redistributions of source code must retain the above copyright
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer in the
|
11
|
+
# documentation and/or other materials provided with the distribution.
|
12
|
+
# 3. The name of the author may not be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
18
|
+
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
19
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
20
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
21
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
22
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
24
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
|
26
|
+
require 'flv/amf_string_buffer'
|
27
|
+
require 'miyaml'
|
28
|
+
|
29
|
+
module FLV
|
30
|
+
|
31
|
+
class FLVMetaTag < FLVTag
|
32
|
+
|
33
|
+
attr_accessor :meta_data, :event
|
34
|
+
|
35
|
+
def after_initialize(new_object)
|
36
|
+
@tag_type = META
|
37
|
+
unless new_object
|
38
|
+
meta_data_stream = AMFStringBuffer.new(@data)
|
39
|
+
@event = meta_data_stream.read__AMF_data
|
40
|
+
@meta_data = meta_data_stream.read__AMF_data
|
41
|
+
else
|
42
|
+
@event = 'onMetaData'
|
43
|
+
@meta_data = {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def name
|
48
|
+
"Meta Tag (#{@event})"
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_meta_data(meta_data)
|
52
|
+
return nil if meta_data.nil?
|
53
|
+
@metadata.update meta_data
|
54
|
+
end
|
55
|
+
|
56
|
+
def data
|
57
|
+
meta_data_stream = AMFStringBuffer.new('')
|
58
|
+
meta_data_stream.write__AMF_string @event
|
59
|
+
meta_data_stream.write__AMF_data @meta_data
|
60
|
+
meta_data_stream.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
def [](key)
|
64
|
+
@meta_data[key]
|
65
|
+
end
|
66
|
+
|
67
|
+
def []=(key, value)
|
68
|
+
@meta_data[key] = value
|
69
|
+
end
|
70
|
+
|
71
|
+
def inspect
|
72
|
+
out = super
|
73
|
+
out << "event: #{@event}"
|
74
|
+
out << "meta_data:\n #{MiYAML.dump(@meta_data, :indent => 2, :boundaries => false)}"
|
75
|
+
out
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/flv/stream.rb
ADDED
@@ -0,0 +1,492 @@
|
|
1
|
+
# Copyright (c) 2005 Norman Timmler (inlet media e.K., Hamburg, Germany)
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
# 1. Redistributions of source code must retain the above copyright
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer in the
|
11
|
+
# documentation and/or other materials provided with the distribution.
|
12
|
+
# 3. The name of the author may not be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
18
|
+
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
19
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
20
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
21
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
22
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
24
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
|
26
|
+
require 'flv/core_extensions'
|
27
|
+
require 'flv/tag'
|
28
|
+
require 'flv/audio_tag'
|
29
|
+
require 'flv/video_tag'
|
30
|
+
require 'flv/meta_tag'
|
31
|
+
|
32
|
+
|
33
|
+
module FLV
|
34
|
+
|
35
|
+
class FLVError < StandardError; end
|
36
|
+
class FLVTagError < FLVError; end
|
37
|
+
class FLVStreamError < FLVError; end
|
38
|
+
|
39
|
+
class FLVStream
|
40
|
+
|
41
|
+
attr_accessor :signatur,
|
42
|
+
:version,
|
43
|
+
:type_flags_audio,
|
44
|
+
:type_flags_video,
|
45
|
+
:tags,
|
46
|
+
:stream_log
|
47
|
+
|
48
|
+
def initialize(in_stream, out_stream = nil, stream_log = false)
|
49
|
+
|
50
|
+
|
51
|
+
@stream_log = stream_log ? (File.open('stream.log', File::CREAT|File::WRONLY|File::TRUNC) rescue AMFStringBuffer.new) : AMFStringBuffer.new
|
52
|
+
@in_stream = in_stream
|
53
|
+
@out_stream = out_stream || in_stream
|
54
|
+
|
55
|
+
unless eof?
|
56
|
+
begin
|
57
|
+
read_header
|
58
|
+
read_tags
|
59
|
+
rescue Object => e
|
60
|
+
log e
|
61
|
+
raise e
|
62
|
+
ensure
|
63
|
+
@stream_log.close
|
64
|
+
end
|
65
|
+
else
|
66
|
+
@version = 1
|
67
|
+
@type_flags_audio = false
|
68
|
+
@type_flags_video = false
|
69
|
+
@extra_data = ''
|
70
|
+
@tags = []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# general
|
76
|
+
def add_tags(tags, stick_on_framerate = true, overwrite = true)
|
77
|
+
tags = [tags] unless tags.kind_of? Array
|
78
|
+
|
79
|
+
tags.each do |tag|
|
80
|
+
|
81
|
+
# FIXME: Does not really work for video or audio tags, because tags are
|
82
|
+
# inserted next to same kind. Normally audio and video tags are
|
83
|
+
# alternating.
|
84
|
+
if stick_on_framerate && !framerate.nil? &&framerate != 0 && tag.timestamp % (1000 / framerate) != 0
|
85
|
+
raise FLVTagError, "Could not insert tag. Timestamp #{tag.timestamp} does not fit into framerate."
|
86
|
+
next
|
87
|
+
end
|
88
|
+
|
89
|
+
after_tag = @tags.detect { |_tag| _tag.timestamp >= tag.timestamp }
|
90
|
+
|
91
|
+
if after_tag.nil?
|
92
|
+
@tags << tag
|
93
|
+
next
|
94
|
+
end
|
95
|
+
|
96
|
+
if tag.timestamp == after_tag.timestamp && tag.class == after_tag.class
|
97
|
+
if tag.kind_of?(FLVMetaTag) && ( ( tag.event != after_tag.event ) || ( tag.event == after_tag.event && !overwrite ) )
|
98
|
+
@tags.insert( @tags.index(after_tag), tag )
|
99
|
+
else
|
100
|
+
@tags[@tags.index(after_tag)] = tag
|
101
|
+
end
|
102
|
+
else
|
103
|
+
@tags.insert( @tags.index(after_tag), tag )
|
104
|
+
end
|
105
|
+
|
106
|
+
empty_tag_type_cache
|
107
|
+
end
|
108
|
+
|
109
|
+
@tags
|
110
|
+
end
|
111
|
+
|
112
|
+
def cut(options = [])
|
113
|
+
@tags.delete_if { |tag| tag.timestamp < ( options[:in_point] || 0 ) || tag.timestamp > ( options[:out_point] || tags.last.timestamp ) }
|
114
|
+
if options[:collapse]
|
115
|
+
difference = @tags.first.timestamp
|
116
|
+
@tags.each { |tag| tag.timestamp -= difference }
|
117
|
+
end
|
118
|
+
empty_tag_type_cache
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_nearest_keyframe_video_tag(position)
|
122
|
+
keyframe_video_tags.sort do |tag_a, tag_b|
|
123
|
+
(position - tag_a.timestamp).abs <=> (position - tag_b.timestamp).abs
|
124
|
+
end.first
|
125
|
+
end
|
126
|
+
|
127
|
+
def add_meta_tag(meta_data = {})
|
128
|
+
meta_tag = FLVMetaTag.new
|
129
|
+
meta_tag.event = 'onMetaData'
|
130
|
+
|
131
|
+
meta_tag['framerate'] = framerate
|
132
|
+
meta_tag['duration'] = duration
|
133
|
+
meta_tag['lasttimestamp'] = lasttimestamp
|
134
|
+
meta_tag['videosize'] = videosize
|
135
|
+
meta_tag['audiosize'] = audiosize
|
136
|
+
meta_tag['datasize'] = 0 # calculate after tag was added
|
137
|
+
meta_tag['filesize'] = 0 # calculate after tag was added
|
138
|
+
meta_tag['width'] = (width == 0 && on_meta_data_tag) ? on_meta_data_tag.meta_data['width'] : width
|
139
|
+
meta_tag['height'] = (height == 0 && on_meta_data_tag) ? on_meta_data_tag.meta_data['height'] : height
|
140
|
+
meta_tag['videodatarate'] = videodatarate
|
141
|
+
meta_tag['audiodatarate'] = audiodatarate
|
142
|
+
meta_tag['lastkeyframetimestamp'] = lastkeyframetimestamp
|
143
|
+
meta_tag['audiocodecid'] = audiocodecid
|
144
|
+
meta_tag['videocodecid'] = videocodecid
|
145
|
+
meta_tag['audiodelay'] = audiodelay
|
146
|
+
meta_tag['canSeekToEnd'] = canSeekToEnd
|
147
|
+
meta_tag['stereo'] = stereo
|
148
|
+
meta_tag['audiosamplerate'] = audiosamplerate
|
149
|
+
meta_tag['audiosamplesize'] = audiosamplesize
|
150
|
+
meta_tag['cuePoints'] = cue_points
|
151
|
+
meta_tag['keyframes'] = keyframes
|
152
|
+
meta_tag['hasVideo'] = has_video?
|
153
|
+
meta_tag['hasAudio'] = has_audio?
|
154
|
+
meta_tag['hasMetadata'] = true
|
155
|
+
meta_tag['hasCuePoints'] = has_cue_points?
|
156
|
+
meta_tag['hasKeyframes'] = has_keyframes?
|
157
|
+
|
158
|
+
meta_tag.meta_data.merge!(meta_data)
|
159
|
+
|
160
|
+
add_tags(meta_tag)
|
161
|
+
|
162
|
+
# recalculate values those need meta tag data size or presence
|
163
|
+
meta_tag['keyframes'] = keyframes
|
164
|
+
meta_tag['datasize'] = datasize
|
165
|
+
meta_tag['filesize'] = filesize
|
166
|
+
meta_tag['hasMetadata'] = has_meta_data?
|
167
|
+
end
|
168
|
+
|
169
|
+
def write
|
170
|
+
|
171
|
+
begin
|
172
|
+
@out_stream.seek( 0 )
|
173
|
+
rescue Object => e
|
174
|
+
end
|
175
|
+
|
176
|
+
write_header
|
177
|
+
write_tags
|
178
|
+
|
179
|
+
begin
|
180
|
+
@out_stream.truncate( @out_stream.pos )
|
181
|
+
rescue Object => e
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def close
|
186
|
+
@in_stream.close
|
187
|
+
@out_stream.close
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# views on tags
|
192
|
+
|
193
|
+
def empty_tag_type_cache
|
194
|
+
@video_tags_cache = nil
|
195
|
+
@keyframe_video_tags_cache = nil
|
196
|
+
@audio_tags_cache = nil
|
197
|
+
@meta_tags_cache = nil
|
198
|
+
@on_cue_point_tags_cache = nil
|
199
|
+
end
|
200
|
+
|
201
|
+
def video_tags
|
202
|
+
@video_tags_cache ||= @tags.find_all { |tag| tag.kind_of? FLVVideoTag }
|
203
|
+
end
|
204
|
+
|
205
|
+
def keyframe_video_tags
|
206
|
+
@keyframe_video_tags_cache ||= @tags.find_all do |tag|
|
207
|
+
tag.kind_of?(FLVVideoTag) && tag.frame_type == FLVVideoTag::KEYFRAME
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def audio_tags
|
212
|
+
@audio_tags_cache ||= @tags.find_all { |tag| tag.kind_of? FLVAudioTag }
|
213
|
+
end
|
214
|
+
|
215
|
+
def meta_tags
|
216
|
+
@meta_tags_cache ||= @tags.find_all { |tag| tag.kind_of? FLVMetaTag }
|
217
|
+
end
|
218
|
+
|
219
|
+
def on_meta_data_tag
|
220
|
+
@tags.find { |tag| tag.kind_of?(FLVMetaTag) && tag.event == 'onMetaData' } # FIXME: Cannot be cached
|
221
|
+
end
|
222
|
+
|
223
|
+
def on_cue_point_tags
|
224
|
+
@on_cue_point_tags_cache ||= @tags.find_all { |tag| tag.kind_of?(FLVMetaTag) && tag.event == 'onCuePoint' } # FIXME: Cannot be cached
|
225
|
+
end
|
226
|
+
|
227
|
+
def has_video?
|
228
|
+
video_tags.size > 0
|
229
|
+
end
|
230
|
+
|
231
|
+
def has_audio?
|
232
|
+
audio_tags.size > 0
|
233
|
+
end
|
234
|
+
|
235
|
+
def has_meta_data?
|
236
|
+
!on_meta_data_tag.nil?
|
237
|
+
end
|
238
|
+
|
239
|
+
def has_cue_points?
|
240
|
+
on_cue_point_tags.size > 0
|
241
|
+
end
|
242
|
+
|
243
|
+
def has_keyframes?
|
244
|
+
keyframe_video_tags.size > 0
|
245
|
+
end
|
246
|
+
|
247
|
+
# meta data
|
248
|
+
|
249
|
+
# FIXME: Could be less complicate and run faster
|
250
|
+
def frame_sequence
|
251
|
+
return nil unless has_video?
|
252
|
+
raise(FLVStreamError, 'File has to contain at least 2 video tags to calculate frame sequence') if video_tags.length < 2
|
253
|
+
|
254
|
+
@frame_sequence ||=
|
255
|
+
begin
|
256
|
+
sequences = video_tags.collect do |tag| # find all sequences
|
257
|
+
video_tags[video_tags.index(tag) + 1].timestamp - tag.timestamp unless tag == video_tags.last
|
258
|
+
end.compact
|
259
|
+
|
260
|
+
uniq_sequences = (sequences.uniq - [0]).sort # remove 0 and try smallest intervall first
|
261
|
+
|
262
|
+
sequence_appearances = uniq_sequences.collect { |sequence| sequences.find_all { |_sequence| sequence == _sequence }.size } # count apperance of each sequence
|
263
|
+
|
264
|
+
uniq_sequences[ sequence_appearances.index( sequence_appearances.max ) ] # return the sequence that appears most
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def framerate
|
269
|
+
return nil unless has_video?
|
270
|
+
frame_sequence == 0 ? 0 : 1000 / frame_sequence
|
271
|
+
end
|
272
|
+
|
273
|
+
def duration
|
274
|
+
lasttimestamp
|
275
|
+
end
|
276
|
+
|
277
|
+
def lasttimestamp
|
278
|
+
last_tag = if has_video?
|
279
|
+
video_tags.last
|
280
|
+
elsif has_audio?
|
281
|
+
audio_tags.last
|
282
|
+
else
|
283
|
+
tags.last
|
284
|
+
end
|
285
|
+
last_tag.timestamp.nil? ? 0 : last_tag.timestamp / 1000.0
|
286
|
+
end
|
287
|
+
|
288
|
+
def lastkeyframetimestamp
|
289
|
+
return nil unless has_video?
|
290
|
+
(keyframe_video_tags.last.nil? || keyframe_video_tags.last.timestamp.nil?) ? 0 : keyframe_video_tags.last.timestamp / 1000.0
|
291
|
+
end
|
292
|
+
|
293
|
+
def videosize
|
294
|
+
video_tags.inject(0) { |size, tag| size += tag.size }
|
295
|
+
end
|
296
|
+
|
297
|
+
def audiosize
|
298
|
+
audio_tags.inject(0) { |size, tag| size += tag.size }
|
299
|
+
end
|
300
|
+
|
301
|
+
def datasize
|
302
|
+
videosize + audiosize + (meta_tags.inject(0) { |size, tag| size += tag.size})
|
303
|
+
end
|
304
|
+
|
305
|
+
def filesize
|
306
|
+
# header + data + backpointers
|
307
|
+
@data_offset + datasize + ((@tags.length + 1) * 4)
|
308
|
+
end
|
309
|
+
|
310
|
+
def width
|
311
|
+
return nil unless has_video?
|
312
|
+
video_tags.first.width || 0
|
313
|
+
end
|
314
|
+
|
315
|
+
def height
|
316
|
+
return nil unless has_video?
|
317
|
+
video_tags.first.height || 0
|
318
|
+
end
|
319
|
+
|
320
|
+
def videodatarate
|
321
|
+
data_size = video_tags.inject(0) do |size, tag|
|
322
|
+
size += tag.data_size
|
323
|
+
end
|
324
|
+
return data_size == 0 ? 0 : data_size / duration * 8 / 1000 # kBits/sec
|
325
|
+
end
|
326
|
+
|
327
|
+
def audiodatarate
|
328
|
+
data_size = audio_tags.inject(0) do |size, tag|
|
329
|
+
size += tag.data_size
|
330
|
+
end
|
331
|
+
return data_size == 0 ? 0 : data_size / duration * 8 / 1000 # kBits/sec
|
332
|
+
end
|
333
|
+
|
334
|
+
def stereo
|
335
|
+
audio_tags.first && audio_tags.first.sound_type == FLVAudioTag::STEREO
|
336
|
+
end
|
337
|
+
|
338
|
+
def audiosamplerate
|
339
|
+
audio_tags.first && audio_tags.first.sound_rate
|
340
|
+
end
|
341
|
+
|
342
|
+
def audiosamplesize
|
343
|
+
audio_tags.first && audio_tags.first.sound_sample_size
|
344
|
+
end
|
345
|
+
|
346
|
+
def audiocodecid
|
347
|
+
audio_tags.first && audio_tags.first.sound_format
|
348
|
+
end
|
349
|
+
|
350
|
+
def videocodecid
|
351
|
+
return nil unless has_video?
|
352
|
+
video_tags.first.codec_id
|
353
|
+
end
|
354
|
+
|
355
|
+
def audiodelay
|
356
|
+
return 0 unless has_video?
|
357
|
+
video_tags.first.timestamp.nil? ? 0 : video_tags.first.timestamp / 1000.0
|
358
|
+
end
|
359
|
+
|
360
|
+
def canSeekToEnd
|
361
|
+
return true unless has_video?
|
362
|
+
video_tags.last.frame_type == FLVVideoTag::KEYFRAME
|
363
|
+
end
|
364
|
+
|
365
|
+
def keyframes
|
366
|
+
object = Object.new
|
367
|
+
|
368
|
+
calculate_tag_byte_offsets
|
369
|
+
|
370
|
+
object.instance_variable_set( :@times, keyframe_video_tags.collect { |video_tag| video_tag.timestamp / 1000.0 } )
|
371
|
+
object.instance_variable_set( :@filepositions, keyframe_video_tags.collect { |video_tag| video_tag.byte_offset } )
|
372
|
+
|
373
|
+
return object
|
374
|
+
end
|
375
|
+
|
376
|
+
def cue_points
|
377
|
+
on_cue_point_tags.collect { |tag| tag.meta_data }
|
378
|
+
end
|
379
|
+
|
380
|
+
def <<(tags)
|
381
|
+
add_tags tags, true
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
def calculate_tag_byte_offsets
|
386
|
+
@tags.inject(@data_offset + 4) { |offset, tag| tag.byte_offset = offset; offset += 4 + tag.size }
|
387
|
+
end
|
388
|
+
|
389
|
+
def read_header
|
390
|
+
begin
|
391
|
+
@signature = @in_stream.read__STRING(3)
|
392
|
+
log "File signature: #{@signature}"
|
393
|
+
raise(FLVStreamError, 'IO is not a FLV stream. Wrong signature.') if @signature != 'FLV'
|
394
|
+
|
395
|
+
@version = @in_stream.read__UI8
|
396
|
+
log "File version: #{@version}"
|
397
|
+
|
398
|
+
type_flags = @in_stream.read__UI8
|
399
|
+
@type_flags_audio = (type_flags & 4) == 1
|
400
|
+
log "File has audio: #{@type_flags_audio}"
|
401
|
+
|
402
|
+
@type_flags_video = (type_flags & 1) == 1
|
403
|
+
log "File has video: #{@type_flags_video}"
|
404
|
+
|
405
|
+
@data_offset = @in_stream.read__UI32
|
406
|
+
log "File header size: #{@data_offset}"
|
407
|
+
|
408
|
+
@extra_data = @in_stream.read__STRING @data_offset - 9
|
409
|
+
log "File header extra data: #{@extra_data}"
|
410
|
+
|
411
|
+
rescue IOError => e
|
412
|
+
raise IOError, "IO Error while reading FLV header. #{e.message}", e.backtrace
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def write_header
|
417
|
+
begin
|
418
|
+
@out_stream.write__STRING 'FLV'
|
419
|
+
@out_stream.write__UI8 1
|
420
|
+
type_flags = 0
|
421
|
+
type_flags += 4 if has_audio?
|
422
|
+
type_flags += 1 if has_video?
|
423
|
+
@out_stream.write__UI8 type_flags
|
424
|
+
@out_stream.write__UI32 9 + @extra_data.length
|
425
|
+
@out_stream.write__STRING @extra_data
|
426
|
+
rescue IOError => e
|
427
|
+
raise IOError, "IO Error while writing FLV header. #{e.message}", e.backtrace
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def read_tags
|
432
|
+
@tags ||= []
|
433
|
+
|
434
|
+
while true
|
435
|
+
break if eof?
|
436
|
+
previous_tag_length = @in_stream.read__UI32
|
437
|
+
log "Previous tag length: #{previous_tag_length}"
|
438
|
+
|
439
|
+
break if eof?
|
440
|
+
log "Tag number: #{@tags.size + 1}"
|
441
|
+
tag_type = @in_stream.read__UI8
|
442
|
+
log "Tag type: #{FLVTag.type2name(tag_type)}"
|
443
|
+
|
444
|
+
break if eof?
|
445
|
+
case tag_type
|
446
|
+
when FLVTag::AUDIO
|
447
|
+
@tags << FLVAudioTag.new(@in_stream)
|
448
|
+
when FLVTag::VIDEO
|
449
|
+
@tags << FLVVideoTag.new(@in_stream)
|
450
|
+
when FLVTag::META
|
451
|
+
@tags << FLVMetaTag.new(@in_stream)
|
452
|
+
else
|
453
|
+
@tags << FLVTag.new(@in_stream)
|
454
|
+
end
|
455
|
+
|
456
|
+
end
|
457
|
+
|
458
|
+
if $VERBOSE
|
459
|
+
total_known_tags =
|
460
|
+
audio_tags.size + video_tags.size + meta_tags.size
|
461
|
+
out = "Read tags: #{audio_tags.size} audio, #{video_tags.size} video,"
|
462
|
+
out << " #{meta_tags.size} meta,"
|
463
|
+
out << " #{@tags.size - total_known_tags} unknown,"
|
464
|
+
out << " #{@tags.size} total\n"
|
465
|
+
puts out
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
def write_tags
|
470
|
+
|
471
|
+
@out_stream.write__UI32 0
|
472
|
+
|
473
|
+
count = 0
|
474
|
+
@tags.each do |tag|
|
475
|
+
tag.serialize @out_stream
|
476
|
+
@out_stream.write__UI32 tag.size
|
477
|
+
count += 1
|
478
|
+
puts "[#{count}]#{tag.inspect}\n" if $VERBOSE
|
479
|
+
end
|
480
|
+
|
481
|
+
puts "Wrote tags: #{count} total" if $VERBOSE
|
482
|
+
end
|
483
|
+
|
484
|
+
def log(msg)
|
485
|
+
@stream_log << msg.to_s + "\n"
|
486
|
+
end
|
487
|
+
|
488
|
+
def eof?
|
489
|
+
@in_stream.eof?
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|