/*
* Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program 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 version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.mmedia;
import java.io.IOException;
import java.util.Vector;
import javax.microedition.media.Control;
import javax.microedition.media.MediaException;
import javax.microedition.media.PlayerListener;
import javax.microedition.media.control.VideoControl;
import javax.microedition.media.control.FramePositioningControl;
import javax.microedition.media.control.RateControl;
import javax.microedition.media.control.StopTimeControl;
import com.sun.mmedia.protocol.BasicDS;
import javax.microedition.media.protocol.DataSource;
/**
* A player for the GIF89a.
*/
public final class GIFPlayer extends BasicPlayer implements Runnable {
/* Single image decoder */
private GIFImageDecoder imageDecoder;
/* the width of a video frame */
private int videoWidth;
/* the height of a video frame */
private int videoHeight;
/* a full GIF frame, also called the reference frame */
private int[] referenceFrame = null;
/* the play thread */
private Thread playThread; // default is null
/* flag indicating whether to end the play thread.
* done is set to true upon stopping or closing the player
* or when the frame count progresses to the total number
* of frames in this movie.
*/
private boolean done;
/* Used to synchronize access to mediatime- and rate- ralated fields
*/
private Object mediaTimeLock = new Object();
/* the start time in milliseconds.
* startTime is initialized upon start of the play thread.
*/
private long startTime;
/* early threshold value for the display time in milliseconds. */
private long EARLY_THRESHOLD = 100;
/* minimum wait time */
private static final long MIN_WAIT = 50;
/* For zero duration GIFs (e.g. non-animated) wait time between STARTED and END_OF_MEDIA */
private static final long ZERO_DURATION_WAIT = 50;
/* a table of frame durations (default rate) */
private Vector frameTimes;
/* the frame count, shows number of rendered frames, and index of next frame to render */
private int frameCount;
/* Last frame duration while scanning frames */
private int scanFrameTime;
/* elapsed media time since start of stream (default rate) */
private long mediaTimeOffset;
/* the display time of the last read & created frame (default rate) */
private long displayTime; // default is 0
/* the video renderer object for the GIF Player */
private VideoRenderer videoRenderer;
/* the video control object for the GIF Player */
private VideoControl videoControl;
/* the frame positioning control object for the GIF Player */
private FramePosCtrl framePosControl;
/* the rate control object for the GIF Player */
private RateCtrl rateControl;
/* the duration of the movie in microseconds (default rate) */
private long duration;
/* the seek type of the stream: either <code>NOT_SEEKABLE</code>,
* <code>SEEKABLE_TO_START</code> or <code>RANDOM_ACCESSIBLE</code>
*/
private int seekType;
/* the position in the source stream directly after the GIF header */
private long firstFramePos;
/* stopped flag */
private boolean stopped;
/* The lock object of play thread */
private Object playLock = new Object();
/* image data */
private byte[] imageData;
private int imageDataLength;
private int lzwCodeSize;
/**
* Sets the media source
*
* @param source The new source value
* @exception MediaException Description of the Exception
*/
public void setSource(DataSource source)
throws MediaException {
super.setSource(source);
if (source.getContentType() == null) {
((BasicDS)source).setContentType(Configuration.MIME_IMAGE_GIF);
}
}
/**
* Retrieves the specified control object for the
* GIF Player. The following controls are currently
* implemented: VideoControl, FramePositioningControl
* and RateControl and StopTimeControl.
*
* @param type the requested control type.
* @return the control object if available,
* otherwise null.
*/
protected Control doGetControl(String type) {
if (type.startsWith(BasicPlayer.pkgName)) {
type = type.substring(BasicPlayer.pkgName.length());
if (type.equals(BasicPlayer.vicName) ||
type.equals(BasicPlayer.guiName)) {
// video control
return videoControl;
} else if (type.equals(BasicPlayer.fpcName)) {
// frame positioning control
return framePosControl;
} else if (type.equals(BasicPlayer.racName)) {
// rate control
return rateControl;
} else if (type.equals(BasicPlayer.stcName)) {
// stop time control
// StopTimeControl is implemented BasicPlayer,
// the parent class of GIF Player
return this;
}
}
return null;
}
/**
* Retrieves the duration of the GIF movie.
*
* @return the duration in microseconds.
*/
protected long doGetDuration() {
return duration;
}
/**
* Retrieves the current media time.
*
* @return the media time in microseconds.
*/
protected long doGetMediaTime() {
synchronized( mediaTimeLock ) {
long mks = ( 0 != startTime ) ? getMicrosecondsFromStart() : 0;
return ( mediaTimeOffset + mks ) * rateControl.getRate() / 100000;
}
}
private long getMicrosecondsFromStart() {
return (System.currentTimeMillis() - startTime) * 1000;
}
/**
* Sets the media time of the GIF Player.
*
* @param now the new media time.
* @exception MediaException thrown if setMediaTime fails.
* @return the new media time in microseconds.
*/
protected long doSetMediaTime(long now) throws MediaException {
if (seekType == NOT_SEEKABLE)
throw new MediaException("stream not seekable");
if (state == STARTED)
doStop();
if (now > duration)
now = duration;
synchronized( mediaTimeLock ) {
mediaTimeOffset = now * 100000 / rateControl.getRate();
}
try {
int count = framePosControl.mapTimeToFrame(now);
//System.out.println("SetMediaTime to " + now + " (frame = " + count + "), frameCount=" + frameCount);
if (count + 1 < frameCount) {
// rewind to beginning
frameCount = 0;
seekFirstFrame();
}
// skip frames
while (frameCount <= count && getFrame())
// We need to decode all frames to have the correct pixels
// for frames with transparent color
decodeFrame();
displayTime = getDuration(frameCount) / 1000;
//System.out.println("SetMediaTime: displayTime = " + displayTime + "; frameCount=" + frameCount);
renderFrame();
if (state == STARTED)
// restart the player
doStart();
} catch (IOException e) {
throw new MediaException(e.getMessage());
}
return now;
}
/**
* Realizes the GIF Player.
*
* @see Player#realize()
* @exception MediaException Thrown if the <code>Player</code> cannot
* be realized.
*/
protected void doRealize() throws MediaException {
duration = TIME_UNKNOWN;
frameCount = 0;
mediaTimeOffset = 0;
seekType = stream.getSeekType();
// parse GIF header
if (parseHeader()) {
scanFrames();
// initialize video control
videoRenderer = Configuration.getConfiguration().getVideoRenderer(this);
videoControl = (VideoControl)videoRenderer.getVideoControl();
videoRenderer.initRendering(VideoRenderer.XRGB888 |
VideoRenderer.USE_ALPHA,
videoWidth, videoHeight);
// initialize frame positioning control
framePosControl = new FramePosCtrl();
// initialize rate control
rateControl = new RateCtrl();
referenceFrame = null;
} else
throw new MediaException("invalid GIF header");
}
/**
* Prefetches the GIF Player.
*
* @see Player#prefetch()
* @exception MediaException Thrown if the <code>Player</code> cannot
* be prefetched.
*/
protected void doPrefetch() throws MediaException {
if (referenceFrame == null)
referenceFrame = new int[videoWidth * videoHeight];
try {
frameCount = 0;
seekFirstFrame();
// get first frame
if (!getFrame())
throw new MediaException("can't get first frame");
decodeFrame();
// If duration is 0 prepare the last frame once.
if (duration == 0) {
while (getFrame())
decodeFrame();
renderFrame();
}
} catch (IOException e) {
throw new MediaException("can't seek first frame");
}
}
/**
* Starts the GIF Player.
*
* @see Player#start()
* @return true, if the player was started successfully,
* otherwise false.
*/
protected boolean doStart() {
if (duration == 0) { // e.g. for non-animated GIFs
new Thread(new Runnable() {
public void run() {
try {
wait(ZERO_DURATION_WAIT);
} catch (InterruptedException ie) { }
sendEvent(PlayerListener.END_OF_MEDIA, new Long(0));
}
}).start();
} else {
synchronized( mediaTimeLock ) {
startTime = System.currentTimeMillis();
}
if (stopped) {
// wake up existing play thread
stopped = false;
synchronized (playLock) {
playLock.notifyAll();
}
} else {
displayTime = getFrameInterval(frameCount) / 1000;
// Ensure that previous thread has finished
playThreadFinished();
synchronized (playLock) {
if (playThread == null) {
// Check for null is a protection against several
// simultaneous doStart()'s trying to create a new thread.
// But if playThreadFinished() failed to terminate
// playThread, we can have a problem
// create a new play thread
playThread = new Thread(this);
playThread.start();
}
}
}
}
return true;
}
/**
* Stops the GIF Player.
*
* @see Player#stop()
* @exception MediaException Thrown if the <code>Player</code> cannot
* be stoppped.
*/
protected void doStop() throws MediaException {
if (stopped) return;
synchronized (playLock) {
try {
if (playThread != null) {
stopped = true;
playLock.notifyAll();
synchronized( mediaTimeLock ) {
mediaTimeOffset = getMicrosecondsFromStart();
startTime = 0;
}
playLock.wait();
}
} catch (InterruptedException ie) {
//do nothing
}
}
}
/**
* Deallocates the GIF Player.
*
* @see Player#deallocate()
*/
protected void doDeallocate() {
playThreadFinished();
stopped = false;
referenceFrame = null;
}
/**
* Closes the GIF Player.
*
* @see Player#close()
*/
protected void doClose() {
done = true;
if (videoRenderer != null) {
videoRenderer.close();
videoRenderer = null;
}
frameTimes = null;
imageDecoder = null;
imageData = null;
}
/**
* The run method driving the play thread.
*/
public void run() {
done = false;
while (!done) {
if (!stopped)
processFrame();
if (stopped) {
synchronized (playLock) {
playLock.notifyAll();
try {
playLock.wait();
} catch (InterruptedException e) {
// nothing to do
}
}
}
}
if (!stopped && !framePosControl.isActive()) {
// the run loop may have terminated prematurely, possibly
// due to an I/O error...
// In this case, the duration needs to be updated.
if (frameCount < frameTimes.size()) {
duration = getDuration(frameCount);
sendEvent(PlayerListener.DURATION_UPDATED, new Long(duration));
}
// send an end-of-media if the player was not stopped
// and the run loop terminates because the end of media
// was reached.
synchronized( mediaTimeLock ) {
mediaTimeOffset = getMicrosecondsFromStart();
startTime = 0;
sendEvent(PlayerListener.END_OF_MEDIA,
new Long(mediaTimeOffset * rateControl.getRate() / 100000));
}
}
synchronized (playLock) {
playThread = null;
playLock.notifyAll();
}
}
/**
* Stops the GIF player when the stop time has been reached.
*/
private void stopTimeReached() {
// stop the player
synchronized( mediaTimeLock ) {
mediaTimeOffset = getMicrosecondsFromStart();
startTime = 0;
}
stopped = true;
// send STOPPED_AT_TIME event
satev();
}
/**
* Ensures that playThread dies
*/
private void playThreadFinished() {
synchronized (playLock) {
// stop the playThread if it was created and started
if (playThread != null) {
done = true;
// wake up the play thread if it was stopped
playLock.notifyAll();
// wait for the play thread to terminate gracefully
try {
// set maximum wait limit in case anything goes wrong.
playLock.wait(5000);
} catch (InterruptedException e) {
// nothing to do.
}
}
}
}
/**
* Returns the duration in microseconds.
*/
private long getDuration(int frameCount) {
long duration = 0;
for (int i = 0; i < frameCount; i++) {
duration += ((Long)frameTimes.elementAt(i)).longValue();
}
return duration;
}
private long getFrameInterval(int frameCount) {
long interval = 0;
if (frameCount > 0 && frameCount <= frameTimes.size()) {
interval = ((Long)frameTimes.elementAt(frameCount - 1)).longValue();
}
return interval;
}
/**
* Maps media time to the corresponding frame.
*
* Returns the frame number.
*/
private int timeToFrame(long mediaTime) {
int frame = 0;
long elapsedTime = 0;
for (int i = 0; i < frameTimes.size(); i++) {
long interval = ((Long)frameTimes.elementAt(i)).longValue();
elapsedTime += interval;
if (elapsedTime <= mediaTime)
frame++;
else
break;
}
return frame;
}
/**
* Maps a frame to the corresponding media time.
*
* Returns the time in microseconds.
*/
private long frameToTime(int frameNumber) {
long elapsedTime = 0;
for (int i = 0; i < frameTimes.size(); i++) {
long interval = ((Long)frameTimes.elementAt(i)).longValue();
if (i < frameNumber)
elapsedTime += interval;
else
break;
}
return elapsedTime;
}
/**
* Decodes and renders a GIF Frame.
*/
private void processFrame() {
// the media time in milliseconds
long mediaTime = doGetMediaTime() / 1000;
// frame interval in milliseconds
long frameInterval = getFrameInterval(frameCount) / 1000;
//System.out.println("Frame: " + frameCount + ", length: " + frameInterval + ", at: " + mediaTime + ", displayTime: " + displayTime);
if (mediaTime + EARLY_THRESHOLD > displayTime) {
// get the next frame
if (!getFrame()) {
// wait until end of last frame
synchronized (playLock) {
try {
long waitTime = (displayTime - mediaTime) * 100000 / rateControl.getRate();
if (waitTime > 0)
playLock.wait(waitTime);
} catch (InterruptedException e) {
// nothing to do
}
}
done = true;
return;
}
decodeFrame();
// frame interval in milliseconds
frameInterval = getFrameInterval(frameCount) / 1000;
// move display time to end of frame
displayTime += frameInterval;
}
// render last read frame
renderFrame();
// report that stop time has been reached if
// the mediaTime is greater or equal to stop time.
if (stopTime != StopTimeControl.RESET &&
doGetMediaTime() >= stopTime) {
stopTimeReached();
}
if (!stopped) {
// threshold levels in milliseconds
// It makes playback falter if frame intervals differ
//EARLY_THRESHOLD = 250;
//if (frameInterval > 0 && frameInterval < EARLY_THRESHOLD)
// EARLY_THRESHOLD = frameInterval / 2;
mediaTime = doGetMediaTime() / 1000;
if (mediaTime + EARLY_THRESHOLD <= displayTime) {
// wait for a bit
synchronized (playLock) {
try {
if (!done) {
mediaTime = doGetMediaTime() / 1000;
long waitTime = (displayTime - EARLY_THRESHOLD - mediaTime)
* 100000 / rateControl.getRate();
while (!stopped && waitTime > 0) {
if (waitTime > MIN_WAIT) {
playLock.wait(MIN_WAIT);
mediaTime = doGetMediaTime() / 1000;
waitTime = (displayTime - EARLY_THRESHOLD -
mediaTime) * 100000 / rateControl.getRate();
} else {
playLock.wait(waitTime);
waitTime = 0;
}
if (stopTime != StopTimeControl.RESET &&
doGetMediaTime() >= stopTime) {
stopTimeReached();
}
}
}
} catch (InterruptedException e) {
// nothing to do
}
}
}
}
}
/*
* Rewinds the stream to the position of the first frame
* to be able to read it again
*/
private void seekFirstFrame() throws IOException {
if (seekType == RANDOM_ACCESSIBLE) {
// seek to the beginning of the first frame
stream.seek(firstFramePos);
} else { // SEEKABLE_TO_START
// seek to the start of stream and parse the header
stream.seek(0);
parseHeader();
}
imageDecoder.clearImage();
}
private void decodeFrame() {
if (imageData != null && imageDecoder != null && referenceFrame != null)
imageDecoder.decodeImage(lzwCodeSize, imageDataLength, imageData, referenceFrame);
}
/**
* Renders a frame.
*/
private void renderFrame() {
if (referenceFrame != null)
videoRenderer.render(referenceFrame);
}
/**
* Scans the input stream for GIF frames and builds a table
* of frame durations.
*/
private void scanFrames() throws MediaException {
//System.out.println("scanFrames at pos " + stream.tell());
frameCount = 0;
scanFrameTime = 0;
duration = 0;
frameTimes = new Vector();
boolean eos = false;
do {
int id;
try {
id = readUnsignedByte();
//System.out.println("scanFrames: id=" + id);
} catch (IOException e) {
id = 0x3b;
}
if (id == 0x21) {
parseControlExtension(true);
} else if (id == 0x2c) {
parseImageDescriptor(true);
frameCount++;
frameTimes.addElement(new Long(scanFrameTime));
duration += scanFrameTime;
scanFrameTime = 0; // ?? reset to zero
} else if (id == 0x3b) {
eos = true;
} else {
eos = true;
}
} while (!eos);
// reset the frame counter
frameCount = 0;
try {
seekFirstFrame();
} catch (IOException e) {
throw new MediaException(e.getMessage());
}
}
/**
* Reads data from the stream object and constructs a GIF frame.
*
* @return true if the frame was read successfully,
* otherwise false.
*/
private boolean getFrame() {
//System.out.println("getFrame at pos " + stream.tell());
if (stream.tell() == 0)
parseHeader();
boolean eos = false;
imageData = null;
do {
int id;
try {
id = readUnsignedByte();
//System.out.println("getFrame: id=" + id);
} catch (IOException e) {
id = 0x3b;
}
if (id == 0x21) {
parseControlExtension(false);
} else if (id == 0x2c) {
parseImageDescriptor(false);
} else if (id == 0x3b) {
eos = true;
} else {
eos = true;
}
} while (!eos && imageData == null);
if (imageData != null) {
frameCount++;
return true;
}
return false;
}
/**
* Parses the GIF header.
*
* @return true, if the header was parsed successfully
* and the the GIF signature and version are
* correct,
* otherwise false.
*/
private boolean parseHeader() {
//System.out.println("parseHeader at pos " + stream.tell());
byte [] header = new byte[6];
try {
stream.read(header, 0, 6);
} catch (IOException e) {
return false;
}
// check that signature spells GIF
if (header[0] != 'G' || header[1] != 'I' || header[2] != 'F')
return false;
// check that version spells either 87a or 89a
if (header[3] != '8' || header[4] != '7' && header[4] != '9' ||
header[5] != 'a')
return false;
return parseLogicalScreenDescriptor();
}
/**
* Description of the Method
*
* @param bin Description of the Parameter
*/
private boolean parseLogicalScreenDescriptor() {
//System.out.println("parseLogicalScreenDescriptor at pos " + stream.tell());
byte [] logicalScreenDescriptor = new byte[7];
byte [] globalColorTable = null;
try {
stream.read(logicalScreenDescriptor, 0, 7);
} catch (IOException e) {
return false;
}
// logical screen width
videoWidth = readShort(logicalScreenDescriptor, 0);
// logical screen height
videoHeight = readShort(logicalScreenDescriptor, 2);
// flags
int flags = logicalScreenDescriptor[4];
// global color table flag
boolean globalTable = ((flags >> 7) & 0x01) == 1;
// color resolution
int resolution = ((flags >> 4) & 0x07) + 1;
// sort flag: not used in player
//int sortFlag = (flags >> 3) & 0x01;
// global color table depth
int tableDepth = (flags & 0x07) + 1;
// background color index
int index = logicalScreenDescriptor[5] & 0xff;
// pixel aspect ratio: not used inplayer
//int pixelAspectRatio = logicalScreenDescriptor[6];
imageDecoder = new GIFImageDecoder(videoWidth, videoHeight, resolution);
if (globalTable) {
int size = 3 * (1 << tableDepth);
globalColorTable = new byte[size];
try {
stream.read(globalColorTable, 0, size);
} catch (IOException e) {
}
imageDecoder.setGlobalPalette(tableDepth, globalColorTable, index);
}
firstFramePos = stream.tell();
return true;
}
/**
* Reads a 16-bit unsigned short value from data starting
* at the specified offset.
*
* @param data the byte array
* @param offset offset into the byte array
* @return the short value
*/
private int readShort(byte data[], int offset) {
int lo = data[offset] & 0xff;
int hi = data[offset + 1] & 0xff;
return lo + (hi << 8);
}
/**
* Reads a 16-bit unsigned short value from the source stream.
*
* @return the short value
*/
private int readShort() {
int val = 0;
try {
int lo = readUnsignedByte();
int hi = readUnsignedByte();
val = lo + (hi << 8);
} catch (IOException e) {
}
return val;
}
/**
* Parses the Image Descriptor.
*
* Each image in the Data Stream is composed of an Image Descriptor,
* an optional Local Color Table, and the image data. Each image must
* fit within the boundaries of the Logical Screen, as defined in the
* Logical Screen Descriptor.
*/
private void parseImageDescriptor(boolean scan) {
//System.out.println("parseImageDescriptor at pos " + stream.tell());
byte [] imageDescriptor = new byte[9];
byte [] localColorTable = null;
try {
stream.read(imageDescriptor, 0, 9);
} catch (IOException e) {
}
// packed fields
int flags = imageDescriptor[8];
// local color table flag
boolean localTable = ((flags >> 7) & 1) == 1;
int tableDepth = (flags & 0x07) + 1;
if (localTable) {
int size = 3 * (1 << tableDepth);
localColorTable = new byte[size];
try {
stream.read(localColorTable, 0, size);
} catch (IOException e) {
}
}
if (!scan) {
// image left position
int leftPos = readShort(imageDescriptor, 0);
// image top position
int topPos = readShort(imageDescriptor, 2);
// image width
int width = readShort(imageDescriptor, 4);
// image height
int height = readShort(imageDescriptor, 6);
// interlace flag
boolean interlaceFlag = ((flags >> 6) & 0x01) == 1;
// sort flag: not used in player
//int sortFlag = (flags >> 5) & 0x01;
imageDecoder.newFrame(leftPos, topPos, width, height, interlaceFlag);
// local color table size
if (localTable)
imageDecoder.setLocalPalette(tableDepth, localColorTable);
}
parseImageData();
}
/**
* Parses the Image Data.
*
* The image data for a table based image consists of a sequence of
* sub-blocks, of size at most 255 bytes each, containing an index
* into the active color table, for each pixel in the image. Pixel
* indices are in order of left to right and from top to bottom. Each
* index must be within the range of the size of the active color
* table, starting at 0. The sequence of indices is encoded using the
* LZW Algorithm with variable-length code.
*/
private void parseImageData() {
//System.out.println("parseImageData at pos " + stream.tell());
int idx = 0;
try {
lzwCodeSize = readUnsignedByte();
if (imageData == null)
imageData = new byte[1024];
int size;
do {
size = readUnsignedByte();
if (imageData.length < idx + size) {
// increase image data buffer
byte[] data = new byte[ idx + ((size>1024)?size:1024) ];
System.arraycopy(imageData, 0, data, 0, idx);
imageData = data;
}
if (size > 0)
idx += stream.read(imageData, idx, size);
} while (size != 0);
//imageDataLength = idx;
} catch (IOException e) {
//imageDataLength = 0;
}
// Supporting unfinished GIFs
imageDataLength = idx;
//System.out.println("parsed image data bytes: " + idx);
}
/**
* Parses the Plain Text Extension.
*
*/
private void parsePlainTextExtension() {
try {
// block size
int size = readUnsignedByte();
if (size != 12) {
// ERROR
}
// text grid left position
int leftPos = readShort();
// text grid top position
int topPos = readShort();
// text grid width
int width = readShort();
// text grid height
int height = readShort();
// character cell width
int cellWidth = readUnsignedByte();
// character cell height
int cellHeight = readUnsignedByte();
// text foreground color index
int fgIndex = readUnsignedByte();
// text background color index
int bgIndex = readUnsignedByte();
// plain text data
do {
size = readUnsignedByte();
if (size > 0) {
byte[] data = new byte[size];
stream.read(data, 0, size);
}
} while (size != 0);
} catch (IOException e) {
}
}
/**
* Parses the Control Extension.
*
*/
private void parseControlExtension(boolean scan) {
//System.out.println("parseControlExtension at pos " + stream.tell());
try {
int label = readUnsignedByte();
if (label == 0xff) {
parseApplicationExtension();
} else if (label == 0xfe) {
parseCommentExtension();
} else if (label == 0xf9) {
parseGraphicControlExtension(scan);
} else if (label == 0x01) {
parsePlainTextExtension();
} else {
// unkown control extension
}
} catch (IOException e) {
}
}
/**
* Parses the Application Extension.
*
*/
private void parseApplicationExtension() {
//System.out.println("parseApplicationExtension at pos " + stream.tell());
try {
// block size
int size = readUnsignedByte();
if (size != 11) {
// System.out.println("ERROR");
}
// application identifier
byte[] data = new byte[8];
stream.read(data, 0, 8);
// application authentication code
data = new byte[3];
stream.read(data, 0, 3);
do {
size = readUnsignedByte();
if (size > 0) {
data = new byte[size];
stream.read(data, 0, size);
}
} while (size != 0);
} catch (IOException e) {
}
}
/**
* Parses the Comment Extension.
*
*/
private void parseCommentExtension() {
//System.out.println("parseCommentExtension at pos " + stream.tell());
try {
int size;
do {
size = readUnsignedByte();
if (size > 0) {
byte[] data = new byte[size];
stream.read(data, 0, size);
}
} while (size != 0);
} catch (IOException e) {
}
}
/**
* Parses the Graphic Control Extension.
*
*/
private void parseGraphicControlExtension(boolean scan) {
//System.out.println("parseGraphicControlExtension at pos " + stream.tell());
byte [] graphicControl = new byte[6];
try {
stream.read(graphicControl, 0, 6);
} catch (IOException e) {
}
// block size: not used in player - validation only
//int size = graphicControl[0] & 0xff;
//if (size != 4) {
// ERROR: invalid block size in graphic control
//}
if (scan) {
// delay time
scanFrameTime = readShort(graphicControl, 2) * 10000;
} else {
// packed field
int flags = graphicControl[1] & 0xff;
// transparency flag
boolean transparencyFlag = (flags & 0x01) == 1;
// user input: not used in player
//int userInput = (flags & 0x02) == 2;
// undraw mode
int undrawMode = (flags >> 2) & 0x07;
int transparencyColorIndex = -1;
if (transparencyFlag)
// transparent color index
transparencyColorIndex = graphicControl[4] & 0xff;
imageDecoder.setGraphicsControl(undrawMode, transparencyColorIndex);
}
// block terminator: shoud be 0
//int terminator = graphicControl[5] & 0xff;
}
/**
* A byte array designed to hold one byte of data.
* see: readUnsignedByte().
*/
private byte[] oneByte = new byte[1];
/**
* Reads one byte from the source stream.
*/
private int readUnsignedByte() throws IOException {
if (stream.read(oneByte, 0, 1) == -1)
throw new IOException();
return oneByte[0] & 0xff;
}
/**
* Inner class implementing the frame positioning control interface.
*
*/
class FramePosCtrl implements FramePositioningControl {
/**
* indicates whether the frame positioning control
* is actively engaged.
*/
private boolean active;
/**
* The constructor of FramePosCtrl.
*/
FramePosCtrl() {
active = false;
}
/**
* Seek to a given video frame.
* The media time of the <code>Player</code> will be updated
* to reflect the new position set.
* <p>
* This method can be called on a stopped or started
* <code>Player</code>.
* If the <code>Player</code> is
* in the <i>Started</i> state, this method may cause the
* <code>Player</code> to change states. If that happens, the
* appropriate transition events will be posted by
* the <code>Player</code> when its state changes.
* <p>
* If the given frame number is less than the first or larger
* than the last frame number in the media, <code>seek</code>
* will jump to either the first or the last frame respectively.
*
* @param frameNumber the frame to seek to.
* @return the actual frame that the Player has seeked to.
*/
public int seek(int frameNumber) {
active = true;
// clear the End-of-media flag to ensure that
// a consecutive start call will start the player
// from the seek position and not from the first
// frame.
EOM = false;
if (frameNumber < 0) {
frameNumber = 0;
} else if (frameNumber >= frameTimes.size()) {
frameNumber = frameTimes.size() - 1;
}
long time = mapFrameToTime(frameNumber);
try {
doSetMediaTime(time);
} catch (MediaException e) {
// nothing to do
}
active = false;
return frameNumber;
}
/**
* Skip a given number of frames from the current position.
* The media time of the <code>Player</code> will be updated to
* reflect the new position set.
* <p>
* This method can be called on a stopped or started
* <code>Player</code>.
*
* If the <code>Player</code> is in the <i>Started</i> state,
* the current position is changing. Hence,
* the frame actually skipped to will not be exact.
* <p>
* If the <code>Player</code> is
* in the <i>Started</i> state, this method may cause the
* <code>Player</code> to change states. If that happens, the
* appropriate transition events will be posted.
* <p>
* If the given <code>framesToSkip</code> will cause the position to
* extend beyond the first or last frame, <code>skip</code> will
* jump to the first or last frame respectively.
*
* @param framesToSkip the number of frames to skip from the current
* position. If framesToSkip is positive, it will seek forward
* by framesToSkip number of frames. If framesToSkip is negative,
* it will seek backward by framesToSkip number of frames.
* e.g. skip(-1) will seek backward one frame.
* @return the actual number of frames skipped.
*/
public int skip(int framesToSkip) {
active = true;
// clear the End-of-media flag to ensure that
// a consecutive start call will start the player
// from the seek position and not from the first
// frame.
EOM = false;
int frames_skipped = 0;
int oldFrame = frameCount - 1;
if (oldFrame < 0) {
oldFrame = 0;
} else if (oldFrame >= frameTimes.size()) {
oldFrame = frameTimes.size() - 1;
}
long newFrame = (long)oldFrame + framesToSkip;
if (newFrame < 0) {
newFrame = 0;
} else if (newFrame >= frameTimes.size()) {
newFrame = frameTimes.size() - 1;
}
long time = mapFrameToTime((int)newFrame);
try {
doSetMediaTime(time);
frames_skipped = (int) (newFrame - oldFrame);
} catch (MediaException e) {
// nothing to do
}
active = false;
return frames_skipped;
}
/**
* Converts the given frame number to the corresponding media time.
* The method only performs the calculations. It does not
* position the media to the given frame.
*
* @param frameNumber the input frame number for the conversion.
* @return the converted media time in microseconds for the
* given frame. If the conversion fails, -1 is returned.
*/
public long mapFrameToTime(int frameNumber) {
if (frameNumber < 0 || frameNumber >= frameTimes.size()) {
return -1;
}
return frameToTime(frameNumber);
}
/**
* Converts the given media time to the corresponding frame number.
* The method only performs the calculations. It does not
* position the media to the given media time.
* <p>
* The frame returned is the nearest frame that has a media time
* less than or equal to the given media time.
* <p>
* <code>mapTimeToFrame(0)</code> must not fail and must
* return the frame number of the first frame.
*
* @param mediaTime the input media time for the
* conversion in microseconds.
* @return the converted frame number for the given media time.
* If the conversion fails, -1 is returned.
*/
public int mapTimeToFrame(long mediaTime) {
if (mediaTime < 0 || mediaTime > duration) {
return -1;
}
return timeToFrame(mediaTime);
}
public boolean isActive() {
return active;
}
}
/**
* Inner class implementing the rate control interface.
*
*/
class RateCtrl implements RateControl {
/* the playback rate in 1000 times the percentage of the
* actual rate.
*/
private int rate;
/* the minimum playback rate */
private static final int MIN_PLAYBACK_RATE = 10000; // 10%
/* the maximum playback rate */
private static final int MAX_PLAYBACK_RATE = 200000; // 200%
/**
* The constructor of RateCtrl.
*/
RateCtrl() {
rate = 100000; // normal speed, 100%
}
/**
* Sets the playback rate.
*
* The specified rate is 1000 times the percentage of the
* actual rate. For example, to play back at twice the speed, specify
* a rate of 200'000.<p>
*
* The <code>setRate</code> method returns the actual rate set by the
* <code>Player</code>. <code>Player</code> should set their rate
* as close to the requested
* value as possible, but are not required to set the rate to the exact
* value of any argument other than 100'000. A <code>Player</code>
* is only guaranteed to set
* its rate exactly to 100'000.
* If the given rate is less than <code>getMinRate</code>
* or greater than <code>getMaxRate</code>,
* the rate will be adjusted to the minimum or maximum
* supported rate respectively.
* <p>
* If the <code>Player</code> is already
* started, <code>setRate</code> will immediately take effect.
*
* @param millirate The playback rate to set. The rate is given in
* a "milli-percentage" value.
* @return The actual rate set in "milli-percentage".
* @see #getRate
*/
public int setRate(int millirate) {
synchronized( mediaTimeLock ) {
long oldRate = rate;
if (millirate < MIN_PLAYBACK_RATE) {
rate = MIN_PLAYBACK_RATE;
} else if (millirate > MAX_PLAYBACK_RATE) {
rate = MAX_PLAYBACK_RATE;
} else {
rate = millirate;
}
if (0 != startTime) {
mediaTimeOffset = (long)((System.currentTimeMillis() - startTime)
* 1000 * ((double)oldRate / rate - 1)
+ mediaTimeOffset * (double)oldRate / rate);
}
return rate;
}
}
/**
* Gets the current playback rate.
*
* @return the current playback rate in "milli-percentage".
* @see #setRate
*/
public int getRate() {
return rate;
}
/**
* Gets the maximum playback rate supported by the <code>Player</code>.
*
* @return the maximum rate in "milli-percentage".
*/
public int getMaxRate() {
return MAX_PLAYBACK_RATE;
}
/**
* Gets the minimum playback rate supported by the <code>Player</code>.
*
* @return the minimum rate in "milli-percentage".
*/
public int getMinRate() {
return MIN_PLAYBACK_RATE;
}
}
}