All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.red5.io.flv.FLVReader Maven / Gradle / Ivy

package org.red5.io.flv;

/*
 * RED5 Open Source Flash Server - http://code.google.com/p/red5/
 * 
 * Copyright (c) 2006-2010 by respective authors (see below). All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free Software 
 * Foundation; either version 2.1 of the License, or (at your option) any later 
 * version. 
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this library; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.builder.ToStringBuilder;

import org.apache.mina.core.buffer.IoBuffer;
import org.red5.io.BufferType;
import org.red5.io.IKeyFrameMetaCache;
import org.red5.io.IStreamableFile;
import org.red5.io.ITag;
import org.red5.io.ITagReader;
import org.red5.io.IoConstants;
import org.red5.io.amf.Output;
import org.red5.io.object.Serializer;
//import org.red5.io.utils.HexDump;
import org.red5.io.utils.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A Reader is used to read the contents of a FLV file.
 * NOTE: This class is not implemented as threading-safe. The caller
 * should make sure the threading-safety.
 *
 * @author The Red5 Project ([email protected])
 * @author Dominick Accattato ([email protected])
 * @author Luke Hubbard, Codegent Ltd ([email protected])
 * @author Paul Gregoire, ([email protected])
 */
public class FLVReader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer {

	/**
	 * Logger
	 */
	private static Logger log = LoggerFactory.getLogger(FLVReader.class);

	/**
	 * File
	 */
	private File file;

	/**
	 * File input stream
	 */
	private FileInputStream fis;

	/**
	 * File channel
	 */
	private FileChannel channel;

	private long channelSize;

	/**
	 * Keyframe metadata
	 */
	private KeyFrameMeta keyframeMeta;

	/**
	 * Input byte buffer
	 */
	private IoBuffer in;

	/** Set to true to generate metadata automatically before the first tag. */
	private boolean generateMetadata;

	/** Position of first video tag. */
	private long firstVideoTag = -1;

	/** Position of first audio tag. */
	private long firstAudioTag = -1;

	/** metadata sent flag */
	private boolean metadataSent = false;

	/** Duration in milliseconds. */
	private long duration;

	/** Mapping between file position and timestamp in ms. */
	private HashMap posTimeMap;

	/** Buffer type / style to use **/
	private static BufferType bufferType = BufferType.AUTO;

	private static int bufferSize = 1024;

	/** Use load buffer */
	private boolean useLoadBuf;

	/** Cache for keyframe informations. */
	private static IKeyFrameMetaCache keyframeCache;

	/** The header of this FLV file. */
	private FLVHeader header;

	/** Constructs a new FLVReader. */
	FLVReader() {
	}

	/**
	 * Creates FLV reader from file input stream.
	 *
	 * @param f         File
	 * @throws IOException on error
	 */
	public FLVReader(File f) throws IOException {
		this(f, false);
	}

	/**
	 * Creates FLV reader from file input stream, sets up metadata generation flag.
	 *
	 * @param f                    File input stream
	 * @param generateMetadata     true if metadata generation required, false otherwise
	 * @throws IOException on error
	 */
	public FLVReader(File f, boolean generateMetadata) throws IOException {
		if (null == f) {
			log.warn("Reader was passed a null file");
			log.debug("{}", ToStringBuilder.reflectionToString(this));
		}
		
		this.file = f;
		this.fis = new FileInputStream(f);
		this.generateMetadata = generateMetadata;
		channel = fis.getChannel();
		channelSize = channel.size();
		in = null;
		fillBuffer();
		postInitialize();
	}

	/**
	 * Creates FLV reader from file channel.
	 *
	 * @param channel
	 * @throws IOException on error
	 */
	public FLVReader(FileChannel channel) throws IOException {
		if (null == channel) {
			log.warn("Reader was passed a null channel");
			log.debug("{}", ToStringBuilder.reflectionToString(this));
		}
		if (!channel.isOpen()) {
			log.warn("Reader was passed a closed channel");
			return;
		}
		this.channel = channel;
		channelSize = channel.size();
		//log.debug("Channel size: {}", channelSize);
		if (channel.position() > 0) {
			log.debug("Channel position: {}", channel.position());
			channel.position(0);
		}
		fillBuffer();
		postInitialize();
	}

	/**
	 * Accepts mapped file bytes to construct internal members.
	 *
	 * @param generateMetadata         true if metadata generation required, false otherwise
	 * @param buffer                   IoBuffer
	 */
	public FLVReader(IoBuffer buffer, boolean generateMetadata) {
		this.generateMetadata = generateMetadata;
		in = buffer;
		postInitialize();
	}

	public void setKeyFrameCache(IKeyFrameMetaCache keyframeCache) {
		FLVReader.keyframeCache = keyframeCache;
	}

	/**
	 * Get the remaining bytes that could be read from a file or ByteBuffer.
	 *
	 * @return          Number of remaining bytes
	 */
	private long getRemainingBytes() {
		if (!useLoadBuf) {
			return in.remaining();
		}
		try {
			return channelSize - channel.position() + in.remaining();
		} catch (Exception e) {
			log.error("Error getRemainingBytes", e);
			return 0;
		}
	}

	/**
	 * Get the total readable bytes in a file or ByteBuffer.
	 *
	 * @return          Total readable bytes
	 */
	public long getTotalBytes() {
		if (!useLoadBuf) {
			return in.capacity();
		}
		try {
			return channelSize;
		} catch (Exception e) {
			log.error("Error getTotalBytes", e);
			return 0;
		}
	}

	/**
	 * Get the current position in a file or ByteBuffer.
	 *
	 * @return           Current position in a file
	 */
	private long getCurrentPosition() {
		long pos;
		if (!useLoadBuf) {
			return in.position();
		}
		try {
			if (in != null) {
				pos = (channel.position() - in.remaining());
			} else {
				pos = channel.position();
			}
			return pos;
		} catch (Exception e) {
			log.error("Error getCurrentPosition", e);
			return 0;
		}
	}

	/**
	 * Modifies current position.
	 *
	 * @param pos  Current position in file
	 */
	private void setCurrentPosition(long pos) {
		if (pos == Long.MAX_VALUE) {
			pos = file.length();
		}
		if (!useLoadBuf) {
			in.position((int) pos);
			return;
		}
		try {
			if (pos >= (channel.position() - in.limit()) && pos < channel.position()) {
				in.position((int) (pos - (channel.position() - in.limit())));
			} else {
				channel.position(pos);
				fillBuffer(bufferSize, true);
			}
		} catch (Exception e) {
			log.error("Error setCurrentPosition", e);
		}

	}

	/**
	 * Loads whole buffer from file channel, with no reloading (that is, appending).
	 */
	private void fillBuffer() {
		fillBuffer(bufferSize, false);
	}

	/**
	 * Loads data from channel to buffer.
	 *
	 * @param amount         Amount of data to load with no reloading
	 */
	private void fillBuffer(long amount) {
		fillBuffer(amount, false);
	}

	/**
	 * Load enough bytes from channel to buffer.
	 * After the loading process, the caller can make sure the amount
	 * in buffer is of size 'amount' if we haven't reached the end of channel.
	 *
	 * @param amount The amount of bytes in buffer after returning,
	 * no larger than bufferSize
	 * @param reload Whether to reload or append
	 */
	private void fillBuffer(long amount, boolean reload) {
		try {
			if (amount > bufferSize) {
				amount = bufferSize;
			}
			//log.debug("Buffering amount: {} buffer size: {}", amount, bufferSize);
			// Read all remaining bytes if the requested amount reach the end
			// of channel.
			if (channelSize - channel.position() < amount) {
				amount = channelSize - channel.position();
			}
			if (in == null) {
				switch (bufferType) {
					case HEAP:
						in = IoBuffer.allocate(bufferSize, false);
						break;
					case DIRECT:
						in = IoBuffer.allocate(bufferSize, true);
						break;
					default:
						in = IoBuffer.allocate(bufferSize);
				}
				channel.read(in.buf());
				in.flip();
				useLoadBuf = true;
			}
			if (!useLoadBuf) {
				return;
			}
			if (reload || in.remaining() < amount) {
				if (!reload) {
					in.compact();
				} else {
					in.clear();
				}
				channel.read(in.buf());
				in.flip();
			}
		} catch (Exception e) {
			log.error("Error fillBuffer", e);
		}
	}

	/**
	 * Post-initialization hook, reads keyframe metadata and decodes header (if any).
	 */
	private void postInitialize() {
//		if (log.isDebugEnabled()) {
//			//log.debug("FLVReader 1 - Buffer size: {} position: {} remaining: {}", new Object[] { getTotalBytes(), getCurrentPosition(), getRemainingBytes() });
//		}
		if (getRemainingBytes() >= 9) {
			decodeHeader();
		}
		if (file != null) {
//			keyframeMeta = analyzeKeyFrames();
		}
//		long old = getCurrentPosition();
		//log.debug("Position: {}", old);
	}

	/** {@inheritDoc} */
	public boolean hasVideo() {
		KeyFrameMeta meta = analyzeKeyFrames();
		if (meta == null) {
			return false;
		}
		return (!meta.audioOnly && meta.positions.length > 0);
	}

	/**
	 * Getter for buffer type (auto, direct or heap).
	 *
	 * @return Value for property 'bufferType'
	 */
	public static String getBufferType() {
		switch (bufferType) {
			case AUTO:
				return "auto";
			case DIRECT:
				return "direct";
			case HEAP:
				return "heap";
			default:
				return null;
		}
	}

	/**
	 * Setter for buffer type.
	 *
	 * @param bufferType Value to set for property 'bufferType'
	 */
	public static void setBufferType(String bufferType) {
		int bufferTypeHash = bufferType.hashCode();
		switch (bufferTypeHash) {
			case 3198444: //heap
				//Get a heap buffer from buffer pool
				FLVReader.bufferType = BufferType.HEAP;
				break;
			case -1331586071: //direct
				//Get a direct buffer from buffer pool
				FLVReader.bufferType = BufferType.DIRECT;
				break;
			case 3005871: //auto
				//Let MINA choose
			default:
				FLVReader.bufferType = BufferType.AUTO;
		}
	}

	/**
	 * Getter for buffer size.
	 *
	 * @return Value for property 'bufferSize'
	 */
	public static int getBufferSize() {
		return bufferSize;
	}

	/**
	 * Setter for property 'bufferSize'.
	 *
	 * @param bufferSize Value to set for property 'bufferSize'
	 */
	public static void setBufferSize(int bufferSize) {
		// make sure buffer size is no less than 1024 bytes.
		if (bufferSize < 1024) {
			bufferSize = 1024;
		}
		FLVReader.bufferSize = bufferSize;
	}

	/**
	 * Returns the file buffer.
	 * 
	 * @return  File contents as byte buffer
	 */
	public IoBuffer getFileData() {
		// TODO as of now, return null will disable cache
		// we need to redesign the cache architecture so that
		// the cache is layered underneath FLVReader not above it,
		// thus both tag cache and file cache are feasible.
		return null;
	}

	/** {@inheritDoc} */
	public void decodeHeader() {
		// flv header is 9 bytes
		fillBuffer(9);
		header = new FLVHeader();
		// skip signature
		in.skip(3);
		header.setVersion(in.get());
		header.setTypeFlags(in.get());
		header.setDataOffset(in.getInt());
//		if (log.isDebugEnabled()) {
//			//log.debug("Header: {}", header.toString());
//		}
	}

	/** {@inheritDoc}
	 */
	public IStreamableFile getFile() {
		// TODO wondering if we need to have a reference
		return null;
	}

	/** {@inheritDoc}
	 */
	public int getOffset() {
		// XXX what's the difference from getBytesRead
		return 0;
	}

	/** {@inheritDoc}
	 */
	public long getBytesRead() {
		// XXX should summarize the total bytes read or
		// just the current position?
		return getCurrentPosition();
	}

	/** {@inheritDoc} */
	public long getDuration() {
		return duration;
	}

	public int getVideoCodecId() {
		KeyFrameMeta meta = analyzeKeyFrames();
		if (meta == null) {
			return -1;
		}
		long old = getCurrentPosition();
		setCurrentPosition(firstVideoTag);
		readTagHeader();
		fillBuffer(1);
		byte frametype = in.get();
		setCurrentPosition(old);
		return frametype & MASK_VIDEO_CODEC;
	}

	public int getAudioCodecId() {
		KeyFrameMeta meta = analyzeKeyFrames();
		if (meta == null) {
			return -1;
		}
		long old = getCurrentPosition();
		setCurrentPosition(firstAudioTag);
		readTagHeader();
		fillBuffer(1);
		byte frametype = in.get();
		setCurrentPosition(old);
		return frametype & MASK_SOUND_FORMAT;
	}

	/** {@inheritDoc}
	 */
	public boolean hasMoreTags() {
		return getRemainingBytes() > 4;
	}

	/**
	 * Create tag for metadata event.
	 *
	 * @return         Metadata event tag
	 */
	private ITag createFileMeta() {
		// Create tag for onMetaData event
		IoBuffer buf = IoBuffer.allocate(1024);
		buf.setAutoExpand(true);
		Output out = new Output(buf);
		// Duration property
		out.writeString("onMetaData");
		Map props = new HashMap();
		props.put("duration", duration / 1000.0);
		if (firstVideoTag != -1) {
			long old = getCurrentPosition();
			setCurrentPosition(firstVideoTag);
			readTagHeader();
			fillBuffer(1);
			byte frametype = in.get();
			// Video codec id
			props.put("videocodecid", frametype & MASK_VIDEO_CODEC);
			setCurrentPosition(old);
		}
		if (firstAudioTag != -1) {
			long old = getCurrentPosition();
			setCurrentPosition(firstAudioTag);
			readTagHeader();
			fillBuffer(1);
			byte frametype = in.get();
			// Audio codec id
			props.put("audiocodecid", (frametype & MASK_SOUND_FORMAT) >> 4);
			setCurrentPosition(old);
		}
		props.put("canSeekToEnd", true);
		out.writeMap(props, new Serializer());
		buf.flip();

		ITag result = new Tag(IoConstants.TYPE_METADATA, 0, buf.limit(), null, 0);
		result.setBody(buf);
		metadataSent = true;
		//
		out = null;
		return result;
	}

	/** {@inheritDoc}
	 */
	public synchronized ITag readTag() {
		long oldPos = getCurrentPosition();
		ITag tag = readTagHeader();
//		log.debug("readTag, oldPos: {}, tag header: \n{}", oldPos, tag);
		if (!metadataSent && tag.getDataType() != TYPE_METADATA && generateMetadata) {
			// Generate initial metadata automatically
			setCurrentPosition(oldPos);
			KeyFrameMeta meta = analyzeKeyFrames();
			if (meta != null) {
				return createFileMeta();
			}
		}
		int bodySize = tag.getBodySize();
		IoBuffer body = IoBuffer.allocate(bodySize, false);
		// XXX Paul: this assists in 'properly' handling damaged FLV files		
		long newPosition = getCurrentPosition() + bodySize;
		if (newPosition <= getTotalBytes()) {
			int limit;
			while (getCurrentPosition() < newPosition) {
				fillBuffer(newPosition - getCurrentPosition());
				if (getCurrentPosition() + in.remaining() > newPosition) {
					limit = in.limit();
					in.limit((int) (newPosition - getCurrentPosition()) + in.position());
					body.put(in);
					in.limit(limit);
				} else {
					body.put(in);
				}
			}
			body.flip();
			tag.setBody(body);
		}
		if (tag.getDataType() == TYPE_METADATA) {
			metadataSent = true;
		}
		return tag;
	}

	/** {@inheritDoc}
	 */
	public void close() {
		log.debug("Reader close");
		if (in != null) {
			in.free();
			in = null;
		}
		if (channel != null) {
			try {
				channel.close();
				fis.close();
			} catch (IOException e) {
				log.error("FLVReader :: close ::>\n", e);
			}
		}
	}

	/**
	 * Key frames analysis may be used as a utility method so
	 * synchronize it.
	 *
	 * @return             Keyframe metadata
	 */
	public synchronized KeyFrameMeta analyzeKeyFrames() {
		if (keyframeMeta != null) {
			return keyframeMeta;
		}
		// check for cached keyframe informations
		if (keyframeCache != null) {
			keyframeMeta = keyframeCache.loadKeyFrameMeta(file);
			if (keyframeMeta != null) {
				// Keyframe data loaded, create other mappings
				duration = keyframeMeta.duration;
				posTimeMap = new HashMap();
				for (int i = 0; i < keyframeMeta.positions.length; i++) {
					posTimeMap.put(keyframeMeta.positions[i], (long) keyframeMeta.timestamps[i]);
				}
				return keyframeMeta;
			}
		}
		// Lists of video positions and timestamps
		List positionList = new ArrayList();
		List timestampList = new ArrayList();
		// Lists of audio positions and timestamps
		List audioPositionList = new ArrayList();
		List audioTimestampList = new ArrayList();
		long origPos = getCurrentPosition();
		// point to the first tag
		setCurrentPosition(9);

		boolean audioOnly = true;
		while (this.hasMoreTags()) {
			long pos = getCurrentPosition();
			// Read tag header and duration
			ITag tmpTag = this.readTagHeader();
			duration = tmpTag.getTimestamp();
			if (tmpTag.getDataType() == IoConstants.TYPE_VIDEO) {
				if (audioOnly) {
					audioOnly = false;
					audioPositionList.clear();
					audioTimestampList.clear();
				}
				if (firstVideoTag == -1) {
					firstVideoTag = pos;
				}

				// Grab Frame type
				fillBuffer(1);
				byte frametype = in.get();
				if (((frametype & MASK_VIDEO_FRAMETYPE) >> 4) == FLAG_FRAMETYPE_KEYFRAME) {
					positionList.add(pos);
					timestampList.add(tmpTag.getTimestamp());
				}

			} else if (tmpTag.getDataType() == IoConstants.TYPE_AUDIO) {
				if (firstAudioTag == -1) {
					firstAudioTag = pos;
				}
				if (audioOnly) {
					audioPositionList.add(pos);
					audioTimestampList.add(tmpTag.getTimestamp());
				}
			}
			// XXX Paul: this 'properly' handles damaged FLV files - as far as
			// duration/size is concerned
			long newPosition = pos + tmpTag.getBodySize() + 15;
			// log.debug("---->" + in.remaining() + " limit=" + in.limit() + "
			// new pos=" + newPosition);
			if (newPosition >= getTotalBytes()) {
				log.error("New position exceeds limit");
//				if (log.isDebugEnabled()) {
//					log.debug("-----");
//					log.debug("Keyframe analysis");
//					log.debug(" data type=" + tmpTag.getDataType() + " bodysize=" + tmpTag.getBodySize());
//					log.debug(" remaining=" + getRemainingBytes() + " limit=" + getTotalBytes() + " new pos=" + newPosition);
//					log.debug(" pos=" + pos);
//					log.debug("-----");
//				}
//				//XXX Paul: A runtime exception is probably not needed here
//				log.info("New position {} exceeds limit {}", newPosition, getTotalBytes());
				//just break from the loop
				break;
			} else {
				setCurrentPosition(newPosition);
			}
		}
		// restore the pos
		setCurrentPosition(origPos);

		keyframeMeta = new KeyFrameMeta();
		keyframeMeta.duration = duration;
		posTimeMap = new HashMap();
		if (audioOnly) {
			// The flv only contains audio tags, use their lists
			// to support pause and seeking
			positionList = audioPositionList;
			timestampList = audioTimestampList;
		}
		keyframeMeta.audioOnly = audioOnly;
		keyframeMeta.positions = new long[positionList.size()];
		keyframeMeta.timestamps = new int[timestampList.size()];
		for (int i = 0; i < keyframeMeta.positions.length; i++) {
			keyframeMeta.positions[i] = positionList.get(i);
			keyframeMeta.timestamps[i] = timestampList.get(i);
			posTimeMap.put((long) positionList.get(i), (long) timestampList.get(i));
		}
		if (keyframeCache != null) {
			keyframeCache.saveKeyFrameMeta(file, keyframeMeta);
		}
		return keyframeMeta;
	}

	/**
	 * Put the current position to pos.
	 * The caller must ensure the pos is a valid one
	 * (eg. not sit in the middle of a frame).
	 *
	 * @param pos         New position in file. Pass Long.MAX_VALUE to seek to end of file.
	 */
	public void position(long pos) {
		setCurrentPosition(pos);
	}

	/**
	 * Read only header part of a tag.
	 *
	 * @return              Tag header
	 */
	private ITag readTagHeader() {
		// previous tag size (4 bytes) + flv tag header size (11 bytes)
		fillBuffer(15);
//		if (log.isDebugEnabled()) {
//			in.mark();
//			StringBuilder sb = new StringBuilder();
//			HexDump.dumpHex(sb, in.array());
//			log.debug("\n{}", sb);
//			in.reset();
//		}		
		// previous tag's size
		int previousTagSize = in.getInt();
		// start of the flv tag
		byte dataType = in.get();
		if (dataType != 8 && dataType != 9 && dataType != 18) {
			log.debug("Invalid data type detected, skipping crap-bytes");
			in.skip(51);			
			dataType = in.get();
		}
//		log.debug("dataType = {}", dataType);
		int bodySize = IOUtils.readUnsignedMediumInt(in);
		int timestamp = IOUtils.readExtendedMediumInt(in);
		if (log.isDebugEnabled()) {
//			int streamId = IOUtils.readUnsignedMediumInt(in);
//			log.debug("Data type: {} timestamp: {} stream id: {} body size: {} previous tag size: {}", new Object[]{dataType, timestamp, streamId, bodySize, previousTagSize});
		} else {
			in.skip(3);
		}
		return new Tag(dataType, timestamp, bodySize, null, previousTagSize);
	}

	public static long getDuration(File flvFile) {
		RandomAccessFile flv = null;
		try {
			flv = new RandomAccessFile(flvFile, "r");
			long flvLength = flv.length();
			if (flvLength < 13) {
				return 0;
			}
			flv.seek(flvLength - 4);
			byte[] buf = new byte[4];
			flv.read(buf);
			long lastTagSize = 0;
			for (int i = 0; i < 4; i++) {
				lastTagSize += (buf[i] & 0x0ff) << ((3 - i) * 8);
			}
			if (lastTagSize == 0) {
				return 0;
			}
			flv.seek(flvLength - lastTagSize);
			flv.read(buf);
			long duration = 0;
			for (int i = 0; i < 3; i++) {
				duration += (buf[i] & 0x0ff) << ((2 - i) * 8);
			}
			duration += (buf[3] & 0x0ff) << 24; // extension byte
			return duration;
		} catch (IOException e) {
			return 0;
		} finally {
			try {
				if (flv != null) {
					flv.close();
				}
			} catch (IOException e) {
			}
			flv = null;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy