/*
* Copyright (C) 2011 in-somnia
*
* This file is part of JAAD.
*
* JAAD 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 3 of the
* License, or (at your option) any later version.
*
* JAAD 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, see <http://www.gnu.org/licenses/>.
*/
package net.sourceforge.jaad.mp4;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.util.Arrays;
public class MP4InputStream {
public static final int MASK8 = 0xFF;
public static final int MASK16 = 0xFFFF;
public static final String UTF8 = "UTF-8";
public static final String UTF16 = "UTF-16";
private static final int BYTE_ORDER_MASK = 0xFEFF;
private final InputStream in;
private final RandomAccessFile fin;
private int peeked;
private long offset; //only used with InputStream
/**
* Constructs an <code>MP4InputStream</code> that reads from an
* <code>InputStream</code>. It will have no random access, thus seeking
* will not be possible.
*
* @param in an <code>InputStream</code> to read from
*/
MP4InputStream(InputStream in) {
this.in = in;
fin = null;
peeked = -1;
offset = 0;
}
/**
* Constructs an <code>MP4InputStream</code> that reads from a
* <code>RandomAccessFile</code>. It will have random access and seeking
* will be possible.
*
* @param in a <code>RandomAccessFile</code> to read from
*/
MP4InputStream(RandomAccessFile fin) {
this.fin = fin;
in = null;
peeked = -1;
}
/**
* Reads the next byte of data from the input. The value byte is returned as
* an int in the range 0 to 255. If no byte is available because the end of
* the stream has been reached, an EOFException is thrown. This method
* blocks until input data is available, the end of the stream is detected,
* or an I/O error occurs.
*
* @return the next byte of data
* @throws IOException If the end of the stream is detected or any I/O error occurs.
*/
public int read() throws IOException {
int i = 0;
if(peeked>=0) {
i = peeked;
peeked = -1;
}
else if(in!=null) i = in.read();
else if(fin!=null) i = fin.read();
if(i==-1) throw new EOFException();
else if(in!=null) offset++;
return i;
}
/**
* Reads <code>len</code> bytes of data from the input into the array
* <code>b</code>. If len is zero, then no bytes are read.
*
* This method blocks until all bytes could be read, the end of the stream
* is detected, or an I/O error occurs.
*
* If the stream ends before <code>len</code> bytes could be read an
* EOFException is thrown.
*
* @param b the buffer into which the data is read.
* @param off the start offset in array <code>b</code> at which the data is written.
* @param len the number of bytes to read.
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
*/
public void read(final byte[] b, int off, int len) throws IOException {
int read = 0;
int i = 0;
if(peeked>=0&&len>0) {
b[off] = (byte) peeked;
peeked = -1;
read++;
}
while(read<len) {
if(in!=null) i = in.read(b, off+read, len-read);
else if(fin!=null) i = fin.read(b, off+read, len-read);
if(i<0) throw new EOFException();
else read += i;
}
offset += read;
}
/**
* Reads up to eight bytes as a long value. This method blocks until all
* bytes could be read, the end of the stream is detected, or an I/O error
* occurs.
*
* @param n the number of bytes to read >0 and <=8
* @return the read bytes as a long value
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
* @throws IndexOutOfBoundsException if <code>n</code> is not in the range
* [1...8] inclusive.
*/
public long readBytes(int n) throws IOException {
if(n<1||n>8) throw new IndexOutOfBoundsException("invalid number of bytes to read: "+n);
final byte[] b = new byte[n];
read(b, 0, n);
long result = 0;
for(int i = 0; i<n; i++) {
result = (result<<8)|(b[i]&0xFF);
}
return result;
}
/**
* Reads data from the input stream and stores them into the buffer array b.
* This method blocks until all bytes could be read, the end of the stream
* is detected, or an I/O error occurs.
* If the length of b is zero, then no bytes are read.
*
* @param b the buffer into which the data is read.
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
*/
public void readBytes(final byte[] b) throws IOException {
read(b, 0, b.length);
}
/**
* Reads <code>n</code> bytes from the input as a String. The bytes are
* directly converted into characters. If not enough bytes could be read, an
* EOFException is thrown.
* This method blocks until all bytes could be read, the end of the stream
* is detected, or an I/O error occurs.
*
* @param n the length of the String.
* @return the String, that was read
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
*/
public String readString(final int n) throws IOException {
int i = -1;
int pos = 0;
char[] c = new char[n];
while(pos<n) {
i = read();
c[pos] = (char) i;
pos++;
}
return new String(c, 0, pos);
}
/**
* Reads a null-terminated UTF-encoded String from the input. The maximum
* number of bytes that can be read before the null must appear must be
* specified.
* Although the method is preferred for unicode, the encoding can be any
* charset name, that is supported by the system.
*
* This method blocks until all bytes could be read, the end of the stream
* is detected, or an I/O error occurs.
*
* @param max the maximum number of bytes to read, before the null-terminator
* must appear.
* @param encoding the charset used to encode the String
* @return the decoded String
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
*/
public String readUTFString(int max, String encoding) throws IOException {
return new String(readTerminated(max, 0), Charset.forName(encoding));
}
/**
* Reads a null-terminated UTF-encoded String from the input. The maximum
* number of bytes that can be read before the null must appear must be
* specified.
* The encoding is detected automatically, it may be UTF-8 or UTF-16
* (determined by a byte order mask at the beginning).
*
* This method blocks until all bytes could be read, the end of the stream
* is detected, or an I/O error occurs.
*
* @param max the maximum number of bytes to read, before the null-terminator
* must appear.
* @return the decoded String
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
*/
public String readUTFString(int max) throws IOException {
//read byte order mask
final byte[] bom = new byte[2];
read(bom, 0, 2);
if(bom[0]==0||bom[1]==0) return new String();
final int i = (bom[0]<<8)|bom[1];
//read null-terminated
final byte[] b = readTerminated(max-2, 0);
//copy bom
byte[] b2 = new byte[b.length+bom.length];
System.arraycopy(bom, 0, b2, 0, bom.length);
System.arraycopy(b, 0, b2, bom.length, b.length);
return new String(b2, Charset.forName((i==BYTE_ORDER_MASK) ? UTF16 : UTF8));
}
/**
* Reads a byte array from the input that is terminated by a specific byte
* (the 'terminator'). The maximum number of bytes that can be read before
* the terminator must appear must be specified.
*
* The terminator will not be included in the returned array.
*
* This method blocks until all bytes could be read, the end of the stream
* is detected, or an I/O error occurs.
*
* @param max the maximum number of bytes to read, before the terminator
* must appear.
* @param terminator the byte that indicates the end of the array
* @return the buffer into which the data is read.
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
*/
public byte[] readTerminated(int max, int terminator) throws IOException {
final byte[] b = new byte[max];
int pos = 0;
int i = 0;
while(pos<max&&i!=-1) {
i = read();
if(i!=-1) b[pos++] = (byte) i;
}
return Arrays.copyOf(b, pos);
}
/**
* Reads a fixed point number from the input. The number is read as a
* <code>m.n</code> value, that results from deviding an integer by
* 2<sup>n</sup>.
*
* @param m the number of bits before the point
* @param n the number of bits after the point
* @return a floating point number with the same value
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
* @throws IllegalArgumentException if the total number of bits (m+n) is not
* a multiple of eight
*/
public double readFixedPoint(int m, int n) throws IOException {
final int bits = m+n;
if((bits%8)!=0) throw new IllegalArgumentException("number of bits is not a multiple of 8: "+(m+n));
final long l = readBytes(bits/8);
final double x = Math.pow(2, n);
double d = ((double) l)/x;
return d;
}
/**
* Skips <code>n</code> bytes in the input. This method blocks until all
* bytes could be skipped, the end of the stream is detected, or an I/O
* error occurs.
*
* @param n the number of bytes to skip
* @throws IOException If the end of the stream is detected, the input
* stream has been closed, or if some other I/O error occurs.
*/
public void skipBytes(final long n) throws IOException {
long l = 0;
if(peeked>=0&&n>0) {
peeked = -1;
l++;
}
while(l<n) {
if(in!=null) l += in.skip((n-l));
else if(fin!=null) l += fin.skipBytes((int) (n-l));
}
offset += l;
}
/**
* Returns the current offset in the stream.
*
* @return the current offset
* @throws IOException if an I/O error occurs (only when using a RandomAccessFile)
*/
public long getOffset() throws IOException {
long l = -1;
if(in!=null) l = offset;
else if(fin!=null) l = fin.getFilePointer();
return l;
}
/**
* Seeks to a specific offset in the stream. This is only possible when
* using a RandomAccessFile. If an InputStream is used, this method throws
* an IOException.
*
* @param pos the offset position, measured in bytes from the beginning of the
* stream
* @throws IOException if an InputStream is used, pos is less than 0 or an
* I/O error occurs
*/
public void seek(long pos) throws IOException {
if(fin!=null) fin.seek(pos);
else throw new IOException("could not seek: no random access");
}
/**
* Indicates, if random access is available. That is, if this
* <code>MP4InputStream</code> was constructed with a RandomAccessFile. If
* this method returns false, seeking is not possible.
*
* @return true if random access is available
*/
public boolean hasRandomAccess() {
return fin!=null;
}
/**
* Indicates, if the input has some data left.
*
* @return true if there is at least one byte left
* @throws IOException if an I/O error occurs
*/
public boolean hasLeft() throws IOException {
final boolean b;
if(fin!=null) b = fin.getFilePointer()<(fin.length()-1);
else if(peeked>=0) b = true;
else {
final int i = in.read();
b = (i!=-1);
if(b) peeked = i;
}
return b;
}
/**
* Closes the input and releases any system resources associated with it.
* Once the stream has been closed, further reading or skipping will throw
* an IOException. Closing a previously closed stream has no effect.
*
* @throws IOException if an I/O error occurs
*/
void close() throws IOException {
peeked = -1;
if(in!=null) in.close();
else if(fin!=null) fin.close();
}
}