/*
* 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 com.sun.j2me.log.Logging;
import com.sun.j2me.log.LogChannels;
import java.io.*;
import java.util.Vector;
import java.util.Enumeration;
import javax.microedition.media.*;
import javax.microedition.media.control.*;
import javax.microedition.media.protocol.*;
import com.sun.mmedia.control.*;
import com.sun.mmedia.protocol.*;
/**
* BasicPlayer provides basic implementation for the Player methods.
* Many of the methods call do<method> to do the actual work that can
* be overridden by subclasses.
*
*/
public abstract class BasicPlayer
implements Player, TimeBase, StopTimeControl
{
/** Unknown media format */
static final String MEDIA_FORMAT_UNKNOWN = "UNKNOWN";
/** Unsupported media format */
static final String MEDIA_FORMAT_UNSUPPORTED = "UNSUPPORTED";
/** Device Tone format */
static final String MEDIA_FORMAT_DEVICE_TONE = "DEVICE_TONE";
/** Device MIDI format */
static final String MEDIA_FORMAT_DEVICE_MIDI = "DEVICE_MIDI";
/** Audio Capture format*/
static final String MEDIA_FORMAT_CAPTURE_AUDIO = "CAPTURE_AUDIO";
/** Video Capture format*/
static final String MEDIA_FORMAT_CAPTURE_VIDEO = "CAPTURE_VIDEO";
/** Radio Capture format*/
static final String MEDIA_FORMAT_CAPTURE_RADIO = "CAPTURE_RADIO";
/** Tone format */
static final String MEDIA_FORMAT_TONE = "TONE";
protected String mediaFormat = null;
protected boolean handledByDevice = false;
protected int hNative; // handle of native API library
protected int pID; // unique ID of the player
protected Player notificationSource = null; // if not null, will substitute 'this'
// in playerUpdate calls
protected MediaDownload mediaDownload = null;
/**
* the state of this player
*/
protected int state = UNREALIZED;
/**
* the loopCount of this player
*/
int loopCountSet = 1, loopCount;
/**
* the flag to indicate whether the Player is currently paused at EOM.
* If true, the Player will seek back to the beginning when
* restarted.
*/
boolean EOM = false;
/**
* the flag to indicate looping after EOM.
*/
boolean loopAfterEOM = false;
/**
* this player's playerlisteners
*/
Vector listeners = new Vector(2);
/**
* Asynchronous event mechanism.
*/
EvtQ evtQ = null;
/**
* event queue lock obj
*/
Object evtLock = new Object();
/**
* Control package name
*/
protected final static String pkgName = "javax.microedition.media.control.";
/**
* Centralized control management with string constants for each
* implemented Control.
* <p>
* For adding a new control interfaces, follow the following steps:
* <ol>
* <li>Add new control name here. If it is not in the package
* javax.microedition.media.control, then the full package
* name of the control must be used.
* <li>Add the control's name field to the allJsr135Ctrls array (see below)
* </ol>
* <p>
* Size note: it would be space saving to declare the array allJsr135Ctrls
* with the strings directly and not define the strings constants here.
* However, it is more convenient for descendants to use
* these constants, instead of e.g.
* <code> allJsr135Ctrls[4]; // RateControl</code>
*
* @see #getControls()
* @see #doGetControl(String)
* @see #allJsr135Ctrls
*/
protected final static String fpcName = "FramePositioningControl";
/**
* Description of the Field
*/
protected final static String mdcName = "MetaDataControl";
/**
* Description of the Field
*/
protected final static String micName = "MIDIControl";
/**
* Description of the Field
*/
protected final static String picName = "PitchControl";
/**
* Description of the Field
*/
protected final static String racName = "RateControl";
/**
* Description of the Field
*/
protected final static String recName = "RecordControl";
/**
* Description of the Field
*/
protected final static String stcName = "StopTimeControl";
/**
* Description of the Field
*/
protected final static String tecName = "TempoControl";
/**
* Description of the Field
*/
protected final static String guiName = "GUIControl";
/**
* Description of the Field
*/
protected final static String vicName = "VideoControl";
/**
* Description of the Field
*/
protected final static String rtspName = "RtspControl";
/**
* Description of the Field
*/
protected final static String tocName = "ToneControl";
/**
* Description of the Field
*/
protected final static String dtocName = "com.sun.mmedia.control.DualToneControl";
/**
* Description of the Field
*/
protected final static String vocName = "VolumeControl";
/**
* An array containing all available JSR-135 controls in Players
* extending BasicPlayer.
*/
private final static String[] allJsr135Ctrls = {
fpcName,
mdcName,
micName,
picName,
racName,
recName,
stcName,
tecName,
guiName,
vicName,
rtspName,
tocName,
dtocName,
vocName,
};
/**
* An array containing all available controls
* in Players extending BasicPlayer, including JSR-234 controls
* if available.
* DO NOT USE DIRECTLY! Use getPossibleControlNames() instead.
*/
private static String[] allCtrls;
/**
* Locator string
*/
protected String locator;
/**
* The input DataSource
*/
protected DataSource source;
/**
* The input SourceStream from the DataSource
*/
protected SourceStream stream;
/**
* The Player's TimeBase.
*/
private TimeBase timeBase = this;
/**
* For StopTimeControl - initially reset
*/
protected long stopTime = StopTimeControl.RESET;
/**
* the default size of the event queue
* can be overridden by descendants
*/
int eventQueueSize = 20;
/**
* flag to prevent delivering events after the CLOSED event
*/
private boolean closedDelivered;
/**
*Constructor for the BasicPlayer object
*/
protected BasicPlayer() {
hNative = 0;
mediaFormat = MEDIA_FORMAT_UNKNOWN;
handledByDevice = false;
pID = 0;
// Initialize sysOffset to the current time.
// This is used for TimeBase calculations.
sysOffset = System.currentTimeMillis() * 1000L;
}
/**
* Check to see if the Player is closed. If the
* unrealized boolean flag is true, check also to
* see if the Player is UNREALIZED.
*
* @param unrealized Description of the Parameter
*/
protected void chkClosed(boolean unrealized) {
if (state == CLOSED || (unrealized && state == UNREALIZED)) {
throw new IllegalStateException("Can't invoke the method at the " +
(state == CLOSED ? "closed" : "unrealized") +
" state");
}
}
/**
* Set the number of times the <code>Player</code> will loop
* and play the content.
* <p>
* By default, the loop count is one. That is, once started,
* the <code>Player</code> will start playing from the current
* media time to the end of media once.
* <p>
* If the loop count is set to N where N is bigger than one,
* starting the <code>Player</code> will start playing the
* content from the current media time to the end of media.
* It will then loop back to the beginning of the content
* (media time zero) and play till the end of the media.
* The number of times it will loop to the beginning and
* play to the end of media will be N-1.
* <p>
* Setting the loop count to 0 is invalid. An
* <code>IllegalArgumentException</code> will be thrown.
* <p>
* Setting the loop count to -1 will loop and play the content
* indefinitely.
* <p>
* If the <code>Player</code> is stopped before the preset loop
* count is reached either because <code>stop</code> is called or
* a preset stop time (set with the <code>StopTimeControl</code>)
* is reached, calling <code>start</code> again will
* resume the looping playback from where it was stopped until it
* fully reaches the preset loop count.
* <p>
* An <i>END_OF_MEDIA</i> event will be posted
* every time the <code>Player</code> reaches the end of media.
* If the <code>Player</code> loops back to the beginning and
* starts playing again because it has not completed the loop
* count, a <i>STARTED</i> event will be posted.
*
* @param count indicates the number of times the content will be
* played. 1 is the default. 0 is invalid. -1 indicates looping
* indefintely.
*/
public void setLoopCount(int count) {
chkClosed(false);
if (state == STARTED) {
throw new IllegalStateException("setLoopCount");
}
if (count == 0 || count < -1) {
throw new IllegalArgumentException("setLoopCount");
}
loopCountSet = count;
loopCount = count;
}
/**
* Description of the Method
*
* @param count Description of the Parameter
*/
protected void doSetLoopCount(int count) {
}
protected void doReceiveRSL()
{
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_MMAPI,
"[basic] received RSL");
}
}
/**
* Constructs portions of the <code>Player</code> without
* acquiring the scarce and exclusive resources.
* This may include examining media data and may
* take some time to complete.
* <p>
* When <code>realize</code> completes successfully,
* the <code>Player</code> is in the
* <i>REALIZED</i> state.
* <p>
* If <code>realize</code> is called when the <code>Player</code> is in
* the <i>REALIZED</i>, <i>PREFETCHTED</i> or <i>STARTED</i> state,
* the request will be ignored.
*
* @exception MediaException Thrown if the <code>Player</code> cannot
* be realized.
*/
public synchronized void realize() throws MediaException {
chkClosed(false);
if (state >= REALIZED) {
return;
}
// BasicPlayer only handles the first stream from
// a DataSource.
if ((source != null) && (stream == null)) {
stream = source.getStreams()[0];
} else {
if (!isCapturePlayer() && !isDevicePlayer()) {
state = UNREALIZED;
throw new MediaException("Unable to realize");
}
}
doRealize();
state = REALIZED;
}
protected boolean hasToneSequenceSet = false;
protected boolean isCapturePlayer()
{
return (mediaFormat.equals(MEDIA_FORMAT_CAPTURE_AUDIO) ||
mediaFormat.equals(MEDIA_FORMAT_CAPTURE_VIDEO) ||
mediaFormat.equals(MEDIA_FORMAT_CAPTURE_RADIO) );
}
protected boolean isDevicePlayer()
{
return (mediaFormat.equals(MEDIA_FORMAT_DEVICE_MIDI) ||
mediaFormat.equals(MEDIA_FORMAT_DEVICE_TONE));
}
/**
* Subclasses need to implement this to realize
* the <code>Player</code>.
*
* @exception MediaException Description of the Exception
*/
protected abstract void doRealize() throws MediaException;
/**
* Acquires the scarce and exclusive resources
* and processes as much data as necessary
* to reduce the start latency.
* <p>
* When <code>prefetch</code> completes successfully,
* the <code>Player</code> is in
* the <i>PREFETCHED</i> state.
* <p>
* If <code>prefetch</code> is called when the <code>Player</code>
* is in the <i>UNREALIZED</i> state,
* it will implicitly call <code>realize</code>.
* <p>
* If <code>prefetch</code> is called when the <code>Player</code>
* is already in the <i>PREFETCHED</i> state, the <code>Player</code>
* may still process data necessary to reduce the start
* latency. This is to guarantee that start latency can
* be maintained at a minimum.
* <p>
* If <code>prefetch</code> is called when the <code>Player</code>
* is in the <i>STARTED</i> state,
* the request will be ignored.
* <p>
* If the <code>Player</code> cannot obtain all
* of the resources it needs, it throws a <code>MediaException</code>.
* When that happens, the <code>Player</code> will not be able to
* start. However, <code>prefetch</code> may be called again when
* the needed resource is later released perhaps by another
* <code>Player</code> or application.
*
* @exception MediaException Thrown if the <code>Player</code> cannot
* be prefetched.
*/
public synchronized void prefetch() throws MediaException {
if (state >= PREFETCHED) {
return;
}
if (state < REALIZED) {
realize();
}
doPrefetch();
VolumeControl vc = (VolumeControl)doGetControl(pkgName + vocName);
if (vc != null && (vc.getLevel() == -1)) {
vc.setLevel(100);
}
state = PREFETCHED;
}
/**
* Subclasses need to implement this to prefetch
* the <code>Player</code>.
*
* @exception MediaException Description of the Exception
*/
protected abstract void doPrefetch() throws MediaException;
/**
* Starts the <code>Player</code> as soon as possible.
* If the <code>Player</code> was previously stopped
* by calling <code>stop</code> or reaching a preset
* stop time, it will resume playback
* from where it was previously stopped. If the
* <code>Player</code> has reached the end of media,
* calling <code>start</code> will automatically
* start the playback from the start of the media.
* <p>
* When <code>start</code> returns successfully,
* the <code>Player</code> must have been started and
* a <code>STARTED</code> event will
* be delivered to the registered <code>PlayerListener</code>s.
* However, the <code>Player</code> is not guaranteed to be in
* the <i>STARTED</i> state. The <code>Player</code> may have
* already stopped (in the <i>PREFETCHED</i> state) because
* the media has 0 or a very short duration.
* <p>
* If <code>start</code> is called when the <code>Player</code>
* is in the <i>UNREALIZED</i> or <i>REALIZED</i> state,
* it will implicitly call <code>prefetch</code>.
* <p>
* If <code>start</code> is called when the <code>Player</code>
* is in the <i>STARTED</i> state,
* the request will be ignored.
*
* @exception MediaException Thrown if the <code>Player</code> cannot
* be started.
*/
public synchronized void start() throws MediaException {
if (state >= STARTED) {
return;
}
if (state < REALIZED) {
realize();
}
if (state < PREFETCHED) {
prefetch();
}
if (isDevicePlayer() && !hasToneSequenceSet) {
sendEvent(PlayerListener.STARTED, new Long(0));
sendEvent( PlayerListener.END_OF_MEDIA, new Long(0) );
return;
}
// Update the time base to use the player's
// media time before starting.
updateTimeBase(true);
// Check for any preset stop time.
if (stopTime != StopTimeControl.RESET) {
if (stopTime <= getMediaTime()) {
satev();
// Send STOPPED_AT_TIME event
return;
}
}
// If it's at the EOM, it will automatically
// loop back to the beginning.
if (EOM) {
try {
setMediaTime(0);
} catch(Exception e) {
// do nothing...
}
}
if (!doStart()) {
throw new MediaException("start");
}
state = STARTED;
sendEvent(PlayerListener.STARTED, new Long(getMediaTime()));
// Finish any pending startup stuff in subclass
// Typically used to start any threads that might potentially
// generate events before the STARTED event is delivered
doPostStart();
}
/**
* Subclasses need to implement this start
* the <code>Player</code>.
*
* @return Description of the Return Value
*/
protected abstract boolean doStart();
/**
* Subclasses can override this method to do the actual starting
* of worker threads.
*/
protected void doPostStart() {
}
protected void continueDownload() {
}
/**
* Stops the <code>Player</code>. It will pause the playback at
* the current media time.
* <p>
* When <code>stop</code> returns, the <code>Player</code> is in the
* <i>PREFETCHED</i> state.
* A <code>STOPPED</code> event will be delivered to the registered
* <code>PlayerListener</code>s.
* <p>
* If <code>stop</code> is called on
* a stopped <code>Player</code>, the request is ignored.
*
* @exception MediaException Thrown if the <code>Player</code>
* cannot be stopped.
*/
public synchronized void stop() throws MediaException {
chkClosed(false);
loopAfterEOM = false;
if (state < STARTED) {
return;
}
doPreStop();
doStop();
// Update the time base to use the system time
// before stopping.
updateTimeBase(false);
state = PREFETCHED;
sendEvent(PlayerListener.STOPPED, new Long(getMediaTime()));
}
/**
* Subclasses need to implement this to realize
* the <code>Player</code>.
*
* @exception MediaException Description of the Exception
*/
protected abstract void doStop() throws MediaException;
/**
* Subclasses can override this method to do pre stop works
*/
protected void doPreStop() {
}
/**
* Release the scarce or exclusive
* resources like the audio device acquired by the <code>Player</code>.
* <p>
* When <code>deallocate</code> returns, the <code>Player</code>
* is in the <i>UNREALIZED</i> or <i>REALIZED</i> state.
* <p>
* If the <code>Player</code> is blocked at
* the <code>realize</code> call while realizing, calling
* <code>deallocate</code> unblocks the <code>realize</code> call and
* returns the <code>Player</code> to the <i>UNREALIZED</i> state.
* Otherwise, calling <code>deallocate</code> returns the
* <code>Player</code> to the <i>REALIZED</i> state.
* <p>
* If <code>deallocate</code> is called when the <code>Player</code>
* is in the <i>UNREALIZED</i> or <i>REALIZED</i>
* state, the request is ignored.
* <p>
* If the <code>Player</code> is <code>STARTED</code>
* when <code>deallocate</code> is called, <code>deallocate</code>
* will implicitly call <code>stop</code> on the <code>Player</code>.
*
*/
public synchronized void deallocate() {
chkClosed(false);
loopAfterEOM = false;
if (state < PREFETCHED) {
return;
}
if (state == STARTED) {
try {
stop();
} catch (MediaException e) {
// Not much we can do here.
}
}
doDeallocate();
if (stream != null) {
// if stream is not seekable, just return
if (NOT_SEEKABLE != stream.getSeekType()) {
try {
// seek to start position
stream.seek(0);
} catch(IOException e) {
// System.out.println("[direct] doDeallocate seek IOException");
}
}
}
state = REALIZED;
}
/**
* Subclasses need to implement this to deallocate
* the <code>Player</code>.
*/
protected abstract void doDeallocate();
/**
* Close the <code>Player</code> and release its resources.
* <p>
* When the method returns, the <code>Player</code> is in the
* <i>CLOSED</i> state and can no longer be used.
* A <code>CLOSED</code> event will be delivered to the registered
* <code>PlayerListener</code>s.
* <p>
* If <code>close</code> is called on a closed <code>Player</code>
* the request is ignored.
*/
public synchronized void close() {
if (state == CLOSED) {
return;
}
if (state == STARTED) {
try {
stop();
} catch (MediaException e) {
// Not much we can do here.
}
}
doDeallocate();
doClose();
state = CLOSED;
if (source != null) {
source.disconnect();
}
sendEvent(PlayerListener.CLOSED, null);
}
/**
* Subclasses need to implement this to close
* the <code>Player</code>.
*/
protected abstract void doClose();
/**
* Sets the <code>Player</code>'s <i>media time</i>.
* <p>
* For some media types, setting the media time may not be very
* accurate. The returned value will indicate the
* actual media time set.
* <p>
* Setting the media time to negative values will effectively
* set the media time to zero. Setting the media time to
* beyond the duration of the media will set the time to
* the end of media.
* <p>
* There are some media types that cannot support the setting
* of media time. Calling <code>setMediaTime</code> will throw
* a <code>MediaException</code> in those cases.
*
* @param now The new media time in microseconds.
* @return The actual media time set in microseconds.
* @exception MediaException Thrown if the media time
* cannot be set.
* @see #getMediaTime
*/
public synchronized long setMediaTime(long now) throws MediaException {
chkClosed(true);
if (now < 0) {
now = 0;
}
// Time-base-time needs to be updated if player is started.
if (state == STARTED) {
origin = getTime();
}
long theDur = doGetDuration();
if ((theDur != TIME_UNKNOWN) && (now > theDur)) {
now = theDur;
}
long rtn = doSetMediaTime(now);
EOM = false;
// Time-base-time needs to be updated if player is started.
if (state == STARTED) {
offset = rtn;
}
return rtn;
}
/**
* Subclasses need to implement this to set the media time
* of the <code>Player</code>.
*
* @param now Description of the Parameter
* @return Description of the Return Value
* @exception MediaException Description of the Exception
*/
protected abstract long doSetMediaTime(long now) throws MediaException;
/**
* Gets this <code>Player</code>'s current <i>media time</i>.
* If the <i>media time</i> cannot be determined,
* <code>getMediaTime</code> returns <code>TIME_UNKNOWN</code>.
*
* @return The current <i>media time</i> in microseconds or
* <code>TIME_UNKNOWN</code>.
* @see #setMediaTime
*/
public long getMediaTime() {
chkClosed(false);
return doGetMediaTime();
}
/**
* Subclasses need to implement this to get the media time
* of the <code>Player</code>
*
* @return Description of the Return Value
*/
protected abstract long doGetMediaTime();
/**
* Gets the current state of this <code>Player</code>.
* The possible states are: <i>UNREALIZED</i>,
* <i>REALIZED</i>, <i>PREFETCHED</i>, <i>STARTED</i>, <i>CLOSED</i>.
*
* @return The <code>Player</code>'s current state.
*/
public int getState() {
return state;
}
/**
* Get the duration of the media.
* The value returned is the media's duration
* when played at the default rate.
* <br>
* If the duration cannot be determined (for example, the
* <code>Player</code> is presenting live
* media) <CODE>getDuration</CODE> returns <CODE>TIME_UNKNOWN</CODE>.
*
* @return The duration in microseconds or <code>TIME_UNKNOWN</code>.
*/
public long getDuration() {
chkClosed(false);
return doGetDuration();
}
/**
* Subclasses need to implement this to get the duration
* of the <code>Player</code>.
*
* @return Description of the Return Value
*/
protected abstract long doGetDuration();
/**
* Add a player listener for this player.
*
* @param playerListener the listener to add.
* If <code>null</code> is used, the request will be ignored.
* @see #removePlayerListener
*/
public void addPlayerListener(PlayerListener playerListener) {
chkClosed(false);
if (playerListener != null) {
listeners.addElement(playerListener);
}
}
/**
* Remove a player listener for this player.
*
* @param playerListener the listener to remove.
* If <code>null</code> is used or the given
* <code>playerListener</code> is not a listener for this
* <code>Player</code>, the request will be ignored.
* @see #addPlayerListener
*/
public void removePlayerListener(PlayerListener playerListener) {
chkClosed(false);
listeners.removeElement(playerListener);
}
/**
* Send event to player
*
* @param evt event type
* @param evtData event data
*/
public void sendEvent(String evt, Object evtData) {
// There's always one listener for EOM -- itself.
// "Deliver" the CLOSED event so that the evtQ thread may terminate
if (listeners.size() == 0 && !evt.equals( PlayerListener.END_OF_MEDIA )
&& !evt.equals( PlayerListener.CLOSED )
&& !evt.equals(PlayerListener.ERROR)) {
return;
}
// Safeguard against sending events after CLOSED event to avoid
// deadlock in event delivery thread.
if (closedDelivered) {
return;
}
// Deliver the event to the listeners.
synchronized (evtLock) {
if (evtQ == null) {
evtQ = new EvtQ(this);
}
evtQ.sendEvent(evt, evtData);
// try to let listener run
Thread.currentThread().yield();
}
if (evt == PlayerListener.CLOSED || evt == PlayerListener.ERROR) {
closedDelivered = true;
}
}
/**
* Description of the Method
*/
synchronized void doLoop() {
// If a loop count is set, we'll loop back to the beginning.
if ((loopCount > 1) || (loopCount == -1)) {
try {
setMediaTime(0);
} catch(MediaException e){
// Do nothing...
}
try {
if (loopCount > 1) {
loopCount--;
}
start();
} catch (MediaException ex) {
// System.out.println("[basic] doLoop exception " + ex.getMessage());
loopCount = 1;
}
} else if (loopCountSet > 1) {
loopCount = loopCountSet;
}
loopAfterEOM = false;
}
// "final" to verify that no subclass overrides getControls.
// can be removed if overload necessary
/**
* Gets the controls attribute of the BasicPlayer object
*
* @return The controls value
*/
public final Control[] getControls() {
chkClosed(true);
Vector v = new Vector(3);
// average maximum number of controls
String [] ctrlNames = getPossibleControlNames();
for (int i = 0; i < ctrlNames.length; i++) {
Object c = getControl(ctrlNames[i]);
if ((c != null) && !v.contains(c)) {
v.addElement(c);
}
}
Control[] ret = new Control[v.size()];
v.copyInto(ret);
return ret;
}
/**
* Gets the <code>Control</code> that supports the specified
* class or interface. The full class
* or interface name should be specified.
* <code>Null</code> is returned if the <code>Control</code>
* is not supported.
*
* @param type Description of the Parameter
* @return <code>Control</code> for the class or interface
* name.
*/
public Control getControl(String type) {
chkClosed(true);
if (type == null) {
throw new IllegalArgumentException();
}
// Prepend the package name if the type given does not
// have the package prefix.
if (type.indexOf('.') < 0) {
// for non-fully qualified control names,
// look up the package in the allJsr135Ctrls array
for (int i = 0; i < allJsr135Ctrls.length; i++) {
if (allJsr135Ctrls[i].equals(type)) {
// standard controls are specified
// without package name in allJsr135Ctrls
return doGetControl(pkgName + type);
} else if (allJsr135Ctrls[i].endsWith(type)) {
// non-standard controls are with
// full package name in allJsr135Ctrls
return doGetControl(allJsr135Ctrls[i]);
}
}
}
return doGetControl(type);
}
/**
* Returns the array containing all available JSR-135 and JSR-234 controls
* in Players extending BasicPlayer.
*/
private static String [] getPossibleControlNames()
{
if( null == allCtrls )
{
String [] ammsCtrlNames =
Jsr234Proxy.getInstance().getJsr234PlayerControlNames();
allCtrls = new
String [ allJsr135Ctrls.length + ammsCtrlNames.length ];
int i = 0;
for( i = 0; i < allJsr135Ctrls.length; i++ )
{
allCtrls[ i ] = allJsr135Ctrls[ i ];
}
for( int j = 0; i < allCtrls.length; i++, j++ )
{
allCtrls[ i ] = ammsCtrlNames[ j ];
}
}
return allCtrls;
}
/**
* The worker method to actually obtain the control.
*
* @param type the class name of the <code>Control</code>.
* @return <code>Control</code> for the class or interface
* name.
*/
protected abstract Control doGetControl(String type);
/**
* Implementation method for VolumeControl
*
* @param ll Description of the Parameter
* @return Description of the Return Value
*/
int doSetLevel(int ll) {
return ll;
}
/**
* MMAPI Full specific methods.
*
* @param source The new source value
* @exception IOException Description of the Exception
* @exception MediaException Description of the Exception
*/
/**
* Sets the media source
*
* @param source The new source value
* @exception MediaException Description of the Exception
*/
public void setSource(DataSource source)
throws MediaException {
this.source = source;
}
/**
* Sets the media locator
*
* @param locator The new source locator
*/
public void setLocator(String locator) {
this.locator = locator;
}
/**
* Gets the media locator
*
* @returns locator Returns current Media Locator
*/
public String getLocator() {
if (locator != null) {
return locator;
} else if(source != null) {
return source.getLocator();
}
return null;
}
/**
* Sets the <code>TimeBase</code> for this <code>Player</code>.
* <p>
* A <code>Player</code> has a default <code>TimeBase</code> that
* is determined by the implementation.
* To reset a <code>Player</code> to its default
* <code>TimeBase</code>, call <code>setTimeBase(null)</code>.
*
* @param master The new <CODE>TimeBase</CODE> or
* <CODE>null</CODE> to reset the <code>Player</code>
* to its default <code>TimeBase</code>.
* @exception MediaException Thrown if
* the specified <code>TimeBase</code> cannot be set on the
* <code>Player</code>.
* @see #getTimeBase
*/
public void setTimeBase(TimeBase master) throws MediaException {
chkClosed(true);
if (state == STARTED) {
throw new IllegalStateException("Cannot call setTimeBase on a player in the STARTED state");
}
if (master == null) {
return;
}
if (master != this) {
throw new MediaException("Incompatible TimeBase");
}
}
/**
* Gets the <code>TimeBase</code> that this <code>Player</code> is using.
*
* @return The <code>TimeBase</code> that this <code>Player</code> is using.
* @see #setTimeBase
*/
public TimeBase getTimeBase() {
chkClosed(true);
return timeBase;
}
/**
* Get the content type of the media that's
* being played back by this <code>Player</code>.
* <p>
* See <a href="Manager.html#content-type">content type</a>
* for the syntax of the content type returned.
*
* @return The content type being played back by this
* <code>Player</code>.
*/
public String getContentType() {
chkClosed(true);
if (source == null) {
// Call helper function to get a content type
return DefaultConfiguration.getContentType(locator);
} else {
return source.getContentType();
}
}
/**
* StopTimeControl base implementation
*
* @return The stopTime value
*/
// NOTE: Right now, BasicPlayer implements StopTimeControl, but only
// provides partial implementation. That means each individual player
// must complete the implementation. I.E., somehow it must keep polling
// current media time and compare it to the set stop time. If the time
// is reached, it should call doStop and satev.
/**
* Returns the current stop time
*
* @return The stopTime value
*/
public long getStopTime() {
return stopTime;
}
/**
* Sets the stop time
*
* @param time The new stopTime value
*/
public synchronized void setStopTime(long time) {
if (state == STARTED) {
/*
* If the stop time has already been set and its being set again,
* throw an exception
*/
if (stopTime != StopTimeControl.RESET && time != StopTimeControl.RESET) {
throw new IllegalStateException("StopTime already set");
}
/*
* If the new stop time is before the current media time,
* stop the player and send an event
*/
if (time < getMediaTime()) {
try {
doPreStop();
doStop();
} catch (MediaException e) {
// Not much we can do here.
}
satev();
/*
* Send STOPPED_AT_TIME event
*/
return;
}
}
stopTime = time;
doSetStopTime(stopTime);
}
/**
* Subclasses can override this to be notified of a change
* in the stop time
*
* @param time Description of the Parameter
*/
protected void doSetStopTime(long time) {
}
/**
* Send STOPPED_AT_TIME event. Call this after stopping the player
*/
protected void satev() {
// Update the time base to use the system time
// before stopping.
updateTimeBase(false);
state = PREFETCHED;
stopTime = StopTimeControl.RESET;
sendEvent(PlayerListener.STOPPED_AT_TIME, new Long(getMediaTime()));
}
/**
* TimeBase related functions.
*/
private long origin = 0;
private long offset = 0;
private long time = 0;
private long sysOffset = 0;
private boolean useSystemTime = true;
private Object timeLock = new Object();
/**
* Get the current time of this <code>TimeBase</code>. The values
* returned must be non-negative and non-decreasing over time.
*
* @return the current <code>TimeBase</code> time in microseconds.
*/
public long getTime() {
synchronized (timeLock) {
if (useSystemTime) {
time = origin + (System.currentTimeMillis() * 1000L -
sysOffset) - offset;
} else {
time = origin + getMediaTime() - offset;
}
return time;
}
}
/**
* This method needs to be called when the Player transitions
* back and forth the STARTED and STOPPED states. This is
* to make sure that the correct time base time can be computed.
*
* @param started Description of the Parameter
*/
public void updateTimeBase(boolean started) {
synchronized (timeLock) {
origin = getTime();
useSystemTime = !started;
if (started) {
// Computes the starting offset based on the media time.
offset = getMediaTime();
} else {
// Computes the starting offset based on the system time.
offset = System.currentTimeMillis() * 1000L - sysOffset;
}
}
}
/**
* The value returned by <code>getSeekType</code> indicating that this
* <code>SourceStream</code> is not seekable.
* <p>
* Value 0 is assigned to <code>NOT_SEEKABLE</code>.
*/
int NOT_SEEKABLE = SourceStream.NOT_SEEKABLE;
/**
* The value returned by <code>getSeekType</code> indicating that this
* <code>SourceStream</code> can be seeked only to the beginning
* of the media stream.
* <p>
* Value 1 is assigned to <code>SEEKABLE_TO_START</code>.
*/
int SEEKABLE_TO_START = SourceStream.SEEKABLE_TO_START;
/**
* The value returned by <code>getSeekType</code> indicating that this
* <code>SourceStream</code> can be seeked anywhere within the media.
* <p>
* Value 2 is assigned to <code>RANDOM_ACCESSIBLE</code>.
*/
int RANDOM_ACCESSIBLE = SourceStream.RANDOM_ACCESSIBLE;
/**
* Reads <code>len</code>
* bytes from the first StreamSource of the DataSource (member
* variable <code>stream</code>.
* <p>
* This method
* blocks until one of the following conditions
* occurs:<p>
* <ul>
* <li><code>len</code> bytes
* of input data are available, in which case
* a normal return is made.
*
* <li>End of file
* is detected, in which case an <code>MediaException</code>
* is thrown.
*
* <li>An I/O error occurs, in
* which case an <code>IOException</code> is thrown.
* </ul>
* <p>
* If <code>len</code> is zero,
* then no bytes are read. Otherwise, the first
* byte read is stored into element <code>b[off]</code>,
* the next one into <code>b[off+1]</code>,
* and so on. The number of bytes read is,
* at most, equal to <code>len</code>.
*
* @param b the buffer into which the data is read.
* @param off an int specifying the offset into the data.
* @param len an int specifying the number of bytes to read.
* @return Description of the Return Value
* @exception IOException if an I/O error occurs.
*/
protected int readFully(byte b[], int off, int len) throws IOException {
int n = 0;
while (n < len) {
int count = stream.read(b, off + n, len - n);
if (count < 0) {
throw new IOException("premature end of stream");
}
n += count;
}
return len;
}
private final static int MAX_SKIP = 2048;
private static byte[] skipArray;
/**
* Description of the Method
*
* @param numBytes Description of the Parameter
* @return Description of the Return Value
* @exception IOException Description of the Exception
*/
protected long skipFully(int numBytes) throws IOException {
long target = stream.tell() + numBytes;
if (stream.getSeekType() == SourceStream.RANDOM_ACCESSIBLE) {
if (stream.seek(target) != target) {
throw new IOException("skipped past end");
}
return numBytes;
}
if (numBytes < 0) {
throw new IOException("bad param");
}
// Allocate memory for skip array
int toSkip = numBytes;
int min = numBytes;
if (min > MAX_SKIP) {
min = MAX_SKIP;
}
if (skipArray == null || skipArray.length < min) {
skipArray = new byte[min];
}
// Skip over numBytes
while (toSkip > 0) {
min = toSkip;
if (min > MAX_SKIP) {
min = MAX_SKIP;
}
if (stream.read(skipArray, 0, min) < min) {
throw new IOException("skipped past end");
}
toSkip -= min;
}
return numBytes;
}
/**
* In source stream, seek to a particular position
*
* @param where the position intended to seek to.
* @return the actual position seeked to.
* @exception IOException Description of the Exception
*/
protected long seekStrm(long where) throws IOException {
return stream.seek(where);
}
/**
* Gets the seekType attribute of the BasicPlayer object
*
* @return The seekType value
*/
protected int getSeekType() {
return stream.getSeekType();
}
}
/**
* The thread that's responsible for delivering Player events.
* This class lives for only 5 secs. If no event comes in
* 5 secs, it will exit.
*
* @created January 13, 2005
*/
class EvtQ extends Thread {
/**
* the player instance
*/
private BasicPlayer p;
/**
* event type array
*/
private String[] evtQ;
/**
* event data array
*/
private Object[] evtDataQ;
/**
* head and tail pointer of the event queue
*/
private int head, tail;
/**
* The constructor
*
* @param p the instance of BasicPlayer intending to post event to
* this event queue.
*/
EvtQ(BasicPlayer p) {
this.p = p;
evtQ = new String[p.eventQueueSize];
evtDataQ = new Object[p.eventQueueSize];
start();
}
/**
* Put an event in the event queue and wake up the thread to
* deliver it. If the event queue is filled, block.
*
* @param evt Description of the Parameter
* @param evtData Description of the Parameter
*/
synchronized void sendEvent(String evt, Object evtData) {
// Wait if the event queue is full.
// This potentially will block the Player's main thread.
while ((head + 1) % p.eventQueueSize == tail) {
try {
wait(1000);
} catch (Exception e) {}
}
evtQ[head] = evt;
evtDataQ[head] = evtData;
if (++head == p.eventQueueSize) {
head = 0;
}
notifyAll();
// try to let other threads run
Thread.currentThread().yield();
}
/**
* Event handling thread.
*/
public void run() {
String evt = "";
Object evtData = null;
boolean evtToGo = false;
// true if there is an event to send
// true if at least one event is sent,
// in case that posting the initial event
// takes a long time
boolean evtSent = false;
for (; ; ) {
synchronized (this) {
// If the queue is empty, we'll wait
if (head == tail) {
try {
wait(1000);
} catch (Exception e) {
}
}
if (head != tail) {
evt = evtQ[tail];
evtData = evtDataQ[tail];
// For garbage collection.
evtDataQ[tail] = null;
evtToGo = true;
if (++tail == p.eventQueueSize) {
tail = 0;
}
notifyAll();
} else {
evtToGo = false;
}
}
// synchronized this
if (evtToGo) {
// First, check and handle EOM.
if (evt == PlayerListener.END_OF_MEDIA) {
synchronized (p) {
p.EOM = true;
p.loopAfterEOM = false;
if (p.state > Player.PREFETCHED) {
p.updateTimeBase(false);
p.state = Player.PREFETCHED;
if (p.loopCount > 1 || p.loopCount == -1) {
p.loopAfterEOM = true;
}
}
}
}
// Notify the PlayerListeners.
Enumeration en;
synchronized (p.listeners) {
en = p.listeners.elements();
}
PlayerListener l;
Player src_p = ( null != p.notificationSource )
? p.notificationSource
: p;
while (en.hasMoreElements()) {
try {
l = (PlayerListener)en.nextElement();
l.playerUpdate(src_p, evt, evtData);
} catch (Exception e) {
e.printStackTrace();
System.err.println("Error in playerUpdate " +
"while delivering event "
+ evt + ": " + e);
}
}
if (p.loopAfterEOM) {
// We'll need to loop back because looping was set.
p.doLoop();
}
evtSent = true;
}
// We'll exit the event thread if we have already sent one
// event and there's no more event after 5 secs; or if the
// Player is closed.
if (evt == PlayerListener.CLOSED || evt == PlayerListener.ERROR) {
// will cause a deadlock if the queue
// is full
synchronized (p.evtLock) {
p.evtQ = null;
break;
// Exit the event thread.
}
}
synchronized (this) {
if (head == tail && evtSent && !evtToGo) {
synchronized (p.evtLock) {
p.evtQ = null;
break;
// Exit the event thread.
}
}
}
}
}
}