blob: f5f4b391eb0ddee49d6ca406d802f6931d8d7e18 [file] [log] [blame]
//
// Copyright (C) 2002 HorizonLive.com, Inc. All Rights Reserved.
// Copyright (C) 2008 Wimba, Inc. All Rights Reserved.
//
// This is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This software 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this software; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
// USA.
//
//
// FbsInputStream.java
//
package com.tightvnc.rfbplayer;
import java.io.*;
import java.util.*;
class FbsInputStream extends InputStream {
protected InputStream in;
protected long startTime;
protected long timeOffset;
protected long seekOffset;
protected boolean farSeeking;
protected boolean paused;
protected boolean isQuitting = false;
protected double playbackSpeed;
protected byte[] buffer;
protected int bufferSize;
protected int bufferPos;
/** The number of bytes to skip in the beginning of the next data block. */
protected long nextBlockOffset;
protected Observer obs;
/**
* Construct FbsInputStream object based on the given InputStream, positioned
* at the very beginning of the corresponding FBS file. This constructor
* reads and checks FBS file signature which would look like "FBS 001.000\n",
* but any decimal number is allowed after the dot.
*
* @param in the InputStream object that will be used as a base for this new
* FbsInputStream instance. It should be positioned at the very beginning of
* the corresponding FBS file, so that first 12 bytes read from the stream
* should form FBS file signature.
* @throws java.io.IOException thrown on read error or on incorrect FBS file
* signature.
*/
FbsInputStream(InputStream in) throws IOException {
this(in, 0, null, 0);
byte[] b = new byte[12];
readFully(b);
if (b[0] != 'F' || b[1] != 'B' || b[2] != 'S' || b[3] != ' ' ||
b[4] != '0' || b[5] != '0' || b[6] != '1' || b[7] != '.' ||
b[8] < '0' || b[8] > '9' || b[9] < '0' || b[9] > '9' ||
b[10] < '0' || b[10] > '9' || b[11] != '\n') {
throw new IOException("Incorrect FBS file signature");
}
}
/**
* Construct FbsInputStream object based on the given byte array and
* continued in the specified InputStream. Arbitrary position in the FBS file
* is allowed.
*
* @param in
* the input stream for reading future data, after <code>buffer</code>
* will be exhausted. The stream should be positioned at any data block
* boundary (byte counter should follow in next four bytes).
* @param timeOffset
* time position corresponding the the data block provided in
* <code>buffer</code>.
* @param buffer
* the data block that will be treated as the beginning of this FBS data
* stream. This byte array is not copied into the new object so it should
* not be altered by the caller afterwards.
* @param nextBlockOffset
* the number of bytes that should be skipped in first data block read
* from <code>in</code>.
*/
FbsInputStream(InputStream in, long timeOffset, byte[] buffer,
long nextBlockOffset) {
this.in = in;
startTime = System.currentTimeMillis() - timeOffset;
this.timeOffset = timeOffset;
seekOffset = -1;
farSeeking = false;
paused = false;
playbackSpeed = 1.0;
this.buffer = buffer;
bufferSize = (buffer != null) ? buffer.length : 0;
bufferPos = 0;
this.nextBlockOffset = nextBlockOffset;
}
// Force stream to finish any wait.
public void quit() {
isQuitting = true;
synchronized(this) {
notify();
}
}
//
// Basic methods overriding InputStream's methods.
//
public int read() throws IOException {
while (bufferSize == 0) {
if (!fillBuffer())
return -1;
}
bufferSize--;
return buffer[bufferPos++] & 0xFF;
}
public int available() throws IOException {
// FIXME: This will work incorrectly if our caller will wait until
// some amount of data is available when the buffer contains less
// data than then that. Current implementation never reads more
// data until the buffer is fully exhausted.
return bufferSize;
}
public synchronized void close() throws IOException {
if (in != null)
in.close();
in = null;
startTime = -1;
timeOffset = 0;
seekOffset = -1;
farSeeking = false;
paused = false;
playbackSpeed = 1.0;
buffer = null;
bufferSize = 0;
bufferPos = 0;
nextBlockOffset = 0;
obs = null;
}
//
// Methods providing additional functionality.
//
public synchronized long getTimeOffset() {
long off = Math.max(seekOffset, timeOffset);
return (long)(off * playbackSpeed);
}
public synchronized void setTimeOffset(long pos, boolean allowJump) {
seekOffset = (long)(pos / playbackSpeed);
if (allowJump) {
long minJumpForwardOffset = timeOffset + (long)(10000 / playbackSpeed);
if (seekOffset < timeOffset || seekOffset > minJumpForwardOffset) {
farSeeking = true;
}
}
notify();
}
public synchronized void setSpeed(double newSpeed) {
long newOffset = (long)(timeOffset * playbackSpeed / newSpeed);
startTime += timeOffset - newOffset;
timeOffset = newOffset;
if (isSeeking()) {
seekOffset = (long)(seekOffset * playbackSpeed / newSpeed);
}
playbackSpeed = newSpeed;
}
public boolean isSeeking() {
return (seekOffset >= 0);
}
public long getSeekOffset() {
return (long)(seekOffset * playbackSpeed);
}
public boolean isPaused() {
return paused;
}
public synchronized void pausePlayback() {
paused = true;
notify();
}
public synchronized void resumePlayback() {
paused = false;
startTime = System.currentTimeMillis() - timeOffset;
notify();
}
public void addObserver(Observer target) {
obs = target;
}
//
// Methods for internal use.
//
private synchronized boolean fillBuffer() throws IOException {
// The reading thread should be interrupted on far seeking.
if (farSeeking)
throw new EOFException("[JUMP]");
// Just wait unless we are performing playback OR seeking.
waitWhilePaused();
if (!readDataBlock()) {
return false;
}
if (seekOffset >= 0) {
if (timeOffset >= seekOffset) {
startTime = System.currentTimeMillis() - seekOffset;
seekOffset = -1;
} else {
return true;
}
}
while (!isQuitting) {
long timeDiff = startTime + timeOffset - System.currentTimeMillis();
if (timeDiff <= 0) {
break;
}
try {
wait(timeDiff);
} catch (InterruptedException e) {
}
waitWhilePaused();
}
return true;
}
/**
* Read FBS data block into the buffer.
* If {@link #nextBlockOffset} is not zero, that number of bytes will be
* skipped in the beginning of the data block.
*
* @return true on success, false if end of file was reached.
* @throws java.io.IOException can be thrown while reading from the
* underlying input stream, or as a result of bad FBS file data.
*/
private boolean readDataBlock() throws IOException {
// Read byte counter, check for EOF condition.
long readResult = readUnsigned32();
if (readResult < 0) {
return false;
}
bufferSize = (int)readResult;
int alignedSize = (bufferSize + 3) & 0xFFFFFFFC;
if (nextBlockOffset > 0) {
in.skip(nextBlockOffset);
bufferSize -= nextBlockOffset;
alignedSize -= nextBlockOffset;
nextBlockOffset = 0;
}
if (bufferSize >= 0) {
buffer = new byte[alignedSize];
readFully(buffer);
bufferPos = 0;
timeOffset = (long)(readUnsigned32() / playbackSpeed);
}
if (bufferSize < 0 || timeOffset < 0 || bufferPos >= bufferSize) {
buffer = null;
bufferSize = 0;
bufferPos = 0;
throw new IOException("Invalid FBS file data");
}
return true;
}
//
// In paused mode, wait for external notification on this object.
//
private void waitWhilePaused() {
while (paused && !isSeeking() && !isQuitting) {
synchronized(this) {
try {
// Note: we call Observer.update(Observable,Object) method
// directly instead of maintaining an Observable object.
obs.update(null, null);
wait();
} catch (InterruptedException e) {
}
}
}
}
private long readUnsigned32() throws IOException {
byte[] buf = new byte[4];
if (!readFully(buf))
return -1;
return ((long)(buf[0] & 0xFF) << 24 |
(buf[1] & 0xFF) << 16 |
(buf[2] & 0xFF) << 8 |
(buf[3] & 0xFF));
}
private boolean readFully(byte[] b) throws IOException {
int off = 0;
int len = b.length;
while (off != len) {
int count = in.read(b, off, len - off);
if (count < 0) {
return false;
}
off += count;
}
return true;
}
}