PNG  IHDR;IDATxܻn0K )(pA 7LeG{ §㻢|ذaÆ 6lذaÆ 6lذaÆ 6lom$^yذag5bÆ 6lذaÆ 6lذa{ 6lذaÆ `}HFkm,mӪôô! x|'ܢ˟;E:9&ᶒ}{v]n&6 h_tڠ͵-ҫZ;Z$.Pkž)!o>}leQfJTu іچ\X=8Rن4`Vwl>nG^is"ms$ui?wbs[m6K4O.4%/bC%t Mז -lG6mrz2s%9s@-k9=)kB5\+͂Zsٲ Rn~GRC wIcIn7jJhۛNCS|j08yiHKֶۛkɈ+;SzL/F*\Ԕ#"5m2[S=gnaPeғL lذaÆ 6l^ḵaÆ 6lذaÆ 6lذa; _ذaÆ 6lذaÆ 6lذaÆ RIENDB` # Copyright (c) 2005 Norman Timmler (inlet media e.K., Hamburg, Germany) # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. require 'flv/core_extensions' require 'flv/tag' require 'flv/audio_tag' require 'flv/video_tag' require 'flv/meta_tag' module FLV class FLVError < StandardError; end class FLVTagError < FLVError; end class FLVStreamError < FLVError; end class FLVStream attr_accessor :signatur, :version, :type_flags_audio, :type_flags_video, :tags, :stream_log def initialize(in_stream, out_stream = nil, stream_log = false) @stream_log = stream_log ? (File.open('stream.log', File::CREAT|File::WRONLY|File::TRUNC) rescue AMFStringBuffer.new) : AMFStringBuffer.new @in_stream = in_stream @out_stream = out_stream || in_stream unless eof? begin read_header read_tags rescue Object => e log e raise e ensure @stream_log.close end else @version = 1 @type_flags_audio = false @type_flags_video = false @extra_data = '' @tags = [] end end # general def add_tags(tags, stick_on_framerate = true, overwrite = true) tags = [tags] unless tags.kind_of? Array tags.each do |tag| # FIXME: Does not really work for video or audio tags, because tags are # inserted next to same kind. Normally audio and video tags are # alternating. if stick_on_framerate && !framerate.nil? &&framerate != 0 && tag.timestamp % (1000 / framerate) != 0 raise FLVTagError, "Could not insert tag. Timestamp #{tag.timestamp} does not fit into framerate." next end after_tag = @tags.detect { |_tag| _tag.timestamp >= tag.timestamp } if after_tag.nil? @tags << tag next end if tag.timestamp == after_tag.timestamp && tag.class == after_tag.class if tag.kind_of?(FLVMetaTag) && ( ( tag.event != after_tag.event ) || ( tag.event == after_tag.event && !overwrite ) ) @tags.insert( @tags.index(after_tag), tag ) else @tags[@tags.index(after_tag)] = tag end else @tags.insert( @tags.index(after_tag), tag ) end empty_tag_type_cache end @tags end def cut(options = []) @tags.delete_if { |tag| tag.timestamp < ( options[:in_point] || 0 ) || tag.timestamp > ( options[:out_point] || tags.last.timestamp ) } if options[:collapse] difference = @tags.first.timestamp @tags.each { |tag| tag.timestamp -= difference } end empty_tag_type_cache end def find_nearest_keyframe_video_tag(position) keyframe_video_tags.sort do |tag_a, tag_b| (position - tag_a.timestamp).abs <=> (position - tag_b.timestamp).abs end.first end def add_meta_tag(meta_data = {}) meta_tag = FLVMetaTag.new meta_tag.event = 'onMetaData' meta_tag['framerate'] = framerate meta_tag['duration'] = duration meta_tag['lasttimestamp'] = lasttimestamp meta_tag['videosize'] = videosize meta_tag['audiosize'] = audiosize meta_tag['datasize'] = 0 # calculate after tag was added meta_tag['filesize'] = 0 # calculate after tag was added meta_tag['width'] = (width == 0 && on_meta_data_tag) ? on_meta_data_tag.meta_data['width'] : width meta_tag['height'] = (height == 0 && on_meta_data_tag) ? on_meta_data_tag.meta_data['height'] : height meta_tag['videodatarate'] = videodatarate meta_tag['audiodatarate'] = audiodatarate meta_tag['lastkeyframetimestamp'] = lastkeyframetimestamp meta_tag['audiocodecid'] = audiocodecid meta_tag['videocodecid'] = videocodecid meta_tag['audiodelay'] = audiodelay meta_tag['canSeekToEnd'] = canSeekToEnd meta_tag['stereo'] = stereo meta_tag['audiosamplerate'] = audiosamplerate meta_tag['audiosamplesize'] = audiosamplesize meta_tag['cuePoints'] = cue_points meta_tag['keyframes'] = keyframes meta_tag['hasVideo'] = has_video? meta_tag['hasAudio'] = has_audio? meta_tag['hasMetadata'] = true meta_tag['hasCuePoints'] = has_cue_points? meta_tag['hasKeyframes'] = has_keyframes? meta_tag.meta_data.merge!(meta_data) add_tags(meta_tag) # recalculate values those need meta tag data size or presence meta_tag['keyframes'] = keyframes meta_tag['datasize'] = datasize meta_tag['filesize'] = filesize meta_tag['hasMetadata'] = has_meta_data? end def write begin @out_stream.seek( 0 ) rescue Object => e end write_header write_tags begin @out_stream.truncate( @out_stream.pos ) rescue Object => e end end def close @in_stream.close @out_stream.close end # views on tags def empty_tag_type_cache @video_tags_cache = nil @keyframe_video_tags_cache = nil @audio_tags_cache = nil @meta_tags_cache = nil @on_cue_point_tags_cache = nil end def video_tags @video_tags_cache ||= @tags.find_all { |tag| tag.kind_of? FLVVideoTag } end def keyframe_video_tags @keyframe_video_tags_cache ||= @tags.find_all do |tag| tag.kind_of?(FLVVideoTag) && tag.frame_type == FLVVideoTag::KEYFRAME end end def audio_tags @audio_tags_cache ||= @tags.find_all { |tag| tag.kind_of? FLVAudioTag } end def meta_tags @meta_tags_cache ||= @tags.find_all { |tag| tag.kind_of? FLVMetaTag } end def on_meta_data_tag @tags.find { |tag| tag.kind_of?(FLVMetaTag) && tag.event == 'onMetaData' } # FIXME: Cannot be cached end def on_cue_point_tags @on_cue_point_tags_cache ||= @tags.find_all { |tag| tag.kind_of?(FLVMetaTag) && tag.event == 'onCuePoint' } # FIXME: Cannot be cached end def has_video? video_tags.size > 0 end def has_audio? audio_tags.size > 0 end def has_meta_data? !on_meta_data_tag.nil? end def has_cue_points? on_cue_point_tags.size > 0 end def has_keyframes? keyframe_video_tags.size > 0 end # meta data # FIXME: Could be less complicate and run faster def frame_sequence return nil unless has_video? raise(FLVStreamError, 'File has to contain at least 2 video tags to calculate frame sequence') if video_tags.length < 2 @frame_sequence ||= begin sequences = video_tags.collect do |tag| # find all sequences video_tags[video_tags.index(tag) + 1].timestamp - tag.timestamp unless tag == video_tags.last end.compact uniq_sequences = (sequences.uniq - [0]).sort # remove 0 and try smallest intervall first sequence_appearances = uniq_sequences.collect { |sequence| sequences.find_all { |_sequence| sequence == _sequence }.size } # count apperance of each sequence uniq_sequences[ sequence_appearances.index( sequence_appearances.max ) ] # return the sequence that appears most end end def framerate return nil unless has_video? frame_sequence == 0 ? 0 : 1000 / frame_sequence end def duration lasttimestamp end def lasttimestamp last_tag = if has_video? video_tags.last elsif has_audio? audio_tags.last else tags.last end last_tag.timestamp.nil? ? 0 : last_tag.timestamp / 1000.0 end def lastkeyframetimestamp return nil unless has_video? (keyframe_video_tags.last.nil? || keyframe_video_tags.last.timestamp.nil?) ? 0 : keyframe_video_tags.last.timestamp / 1000.0 end def videosize video_tags.inject(0) { |size, tag| size += tag.size } end def audiosize audio_tags.inject(0) { |size, tag| size += tag.size } end def datasize videosize + audiosize + (meta_tags.inject(0) { |size, tag| size += tag.size}) end def filesize # header + data + backpointers @data_offset + datasize + ((@tags.length + 1) * 4) end def width return nil unless has_video? video_tags.first.width || 0 end def height return nil unless has_video? video_tags.first.height || 0 end def videodatarate data_size = video_tags.inject(0) do |size, tag| size += tag.data_size end return data_size == 0 ? 0 : data_size / duration * 8 / 1000 # kBits/sec end def audiodatarate data_size = audio_tags.inject(0) do |size, tag| size += tag.data_size end return data_size == 0 ? 0 : data_size / duration * 8 / 1000 # kBits/sec end def stereo audio_tags.first && audio_tags.first.sound_type == FLVAudioTag::STEREO end def audiosamplerate audio_tags.first && audio_tags.first.sound_rate end def audiosamplesize audio_tags.first && audio_tags.first.sound_sample_size end def audiocodecid audio_tags.first && audio_tags.first.sound_format end def videocodecid return nil unless has_video? video_tags.first.codec_id end def audiodelay return 0 unless has_video? video_tags.first.timestamp.nil? ? 0 : video_tags.first.timestamp / 1000.0 end def canSeekToEnd return true unless has_video? video_tags.last.frame_type == FLVVideoTag::KEYFRAME end def keyframes object = Object.new calculate_tag_byte_offsets object.instance_variable_set( :@times, keyframe_video_tags.collect { |video_tag| video_tag.timestamp / 1000.0 } ) object.instance_variable_set( :@filepositions, keyframe_video_tags.collect { |video_tag| video_tag.byte_offset } ) return object end def cue_points on_cue_point_tags.collect { |tag| tag.meta_data } end def <<(tags) add_tags tags, true end private def calculate_tag_byte_offsets @tags.inject(@data_offset + 4) { |offset, tag| tag.byte_offset = offset; offset += 4 + tag.size } end def read_header begin @signature = @in_stream.read__STRING(3) log "File signature: #{@signature}" raise(FLVStreamError, 'IO is not a FLV stream. Wrong signature.') if @signature != 'FLV' @version = @in_stream.read__UI8 log "File version: #{@version}" type_flags = @in_stream.read__UI8 @type_flags_audio = (type_flags & 4) == 1 log "File has audio: #{@type_flags_audio}" @type_flags_video = (type_flags & 1) == 1 log "File has video: #{@type_flags_video}" @data_offset = @in_stream.read__UI32 log "File header size: #{@data_offset}" @extra_data = @in_stream.read__STRING @data_offset - 9 log "File header extra data: #{@extra_data}" rescue IOError => e raise IOError, "IO Error while reading FLV header. #{e.message}", e.backtrace end end def write_header begin @out_stream.write__STRING 'FLV' @out_stream.write__UI8 1 type_flags = 0 type_flags += 4 if has_audio? type_flags += 1 if has_video? @out_stream.write__UI8 type_flags @out_stream.write__UI32 9 + @extra_data.length @out_stream.write__STRING @extra_data rescue IOError => e raise IOError, "IO Error while writing FLV header. #{e.message}", e.backtrace end end def read_tags @tags ||= [] while true break if eof? previous_tag_length = @in_stream.read__UI32 log "Previous tag length: #{previous_tag_length}" break if eof? log "Tag number: #{@tags.size + 1}" tag_type = @in_stream.read__UI8 log "Tag type: #{FLVTag.type2name(tag_type)}" break if eof? case tag_type when FLVTag::AUDIO @tags << FLVAudioTag.new(@in_stream) when FLVTag::VIDEO @tags << FLVVideoTag.new(@in_stream) when FLVTag::META @tags << FLVMetaTag.new(@in_stream) else @tags << FLVTag.new(@in_stream) end end if $VERBOSE total_known_tags = audio_tags.size + video_tags.size + meta_tags.size out = "Read tags: #{audio_tags.size} audio, #{video_tags.size} video," out << " #{meta_tags.size} meta," out << " #{@tags.size - total_known_tags} unknown," out << " #{@tags.size} total\n" puts out end end def write_tags @out_stream.write__UI32 0 count = 0 @tags.each do |tag| tag.serialize @out_stream @out_stream.write__UI32 tag.size count += 1 puts "[#{count}]#{tag.inspect}\n" if $VERBOSE end puts "Wrote tags: #{count} total" if $VERBOSE end def log(msg) @stream_log << msg.to_s + "\n" end def eof? @in_stream.eof? end end end