flvtool2 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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