/*
* @(#)PlaybackEngine.java 1.16 02/08/21
*
* Copyright (c) 1996-2002 Sun Microsystems, Inc. All rights reserved.
*/
package com.sun.media;
import java.io.*;
import java.util.*;
import java.awt.*;
import javax.media.*;
import javax.media.protocol.*;
import javax.media.control.*;
import javax.media.format.*;
import javax.media.renderer.*;
import com.sun.media.util.*;
import com.sun.media.controls.*;
import com.sun.media.renderer.audio.AudioRenderer;
import com.sun.media.codec.video.colorspace.RGBScaler;
/**
* PlaybackEngine implements the media engine for playback.
*/
public class PlaybackEngine extends BasicController implements ModuleListener {
protected BasicPlayer player;
protected DataSource dsource;
protected Vector modules;
protected Vector filters;
protected Vector sinks;
protected Vector waitPrefetched;
protected Vector waitStopped;
protected Vector waitEnded;
protected Vector waitResetted;
protected Track tracks[];
protected Demultiplexer parser;
protected BasicSinkModule masterSink = null;
protected BasicSourceModule source;
protected SlaveClock slaveClock;
private boolean internalErrorOccurred = false;
protected boolean prefetched = false;
protected boolean started = false;
private boolean dataPathBlocked = false;
private boolean useMoreRenderBuffer = false;
private boolean deallocated = false;
public boolean prefetchEnabled = true;
static protected boolean needSavingDB = false;
private Time timeBeforeAbortPrefetch = null;
private float rate = 1.0f;
protected BitRateControl bitRateControl;
protected FrameRateControl frameRateControl;
protected FramePositioningControl framePositioningControl = null;
private long latency = 0;
static boolean DEBUG = true;
protected JMD jmd = null;
protected Container container = null;
static public boolean TRACE_ON = false;
static {
try {
Toolkit.getDefaultToolkit();
} catch (Throwable t) {
DEBUG = false;
}
}
// Controls.
protected BasicTrackControl trackControls[] = new BasicTrackControl[0];
protected ProgressControl progressControl;
private long realizeTime;
private long prefetchTime;
// Error messages.
static String NOT_CONFIGURED_ERROR = "cannot be called before configured";
static String NOT_REALIZED_ERROR = "cannot be called before realized";
static String STARTED_ERROR = "cannot be called after started";
/**
* Turn on memory trace to assit debugging.
*/
static public void setMemoryTrace(boolean on) {
TRACE_ON = on;
}
public PlaybackEngine(BasicPlayer p) {
long initTime = System.currentTimeMillis();
player = p;
createProgressControl();
setClock(slaveClock = new SlaveClock());
stopThreadEnabled = false;
profile("instantiation", initTime);
}
/**
* The PlaybackEngine is configurable.
*/
protected boolean isConfigurable() {
return true;
}
/**
* Verifies to see if the engine accepts the given source.
*/
public void setSource(DataSource ds) throws IOException,
IncompatibleSourceException {
try {
source = BasicSourceModule.createModule(ds);
} catch (IOException ioe) {
Log.warning("Input DataSource: " + ds);
Log.warning(" Failed with IO exception: " + ioe.getMessage());
throw ioe;
} catch (IncompatibleSourceException ise) {
Log.warning("Input DataSource: " + ds);
Log.warning(" is not compatible with the MediaEngine.");
Log.warning(" It's likely that the DataSource is required to extend PullDataSource;");
Log.warning(" and that its source streams implement the Seekable interface ");
Log.warning(" and with random access capability.");
throw ise;
}
if (source == null)
throw new IncompatibleSourceException();
// Create the debugger control
if (DEBUG && jmd == null) {
String jmdTitle = "PlugIn Viewer";
if (ds != null && ds.getLocator() != null) {
jmdTitle = ds.getLocator().toString();
// We'll determine here if we want to allocate a
// render buffer for smoother playback.
// Doing that will increase the latency, so we'll
// only do that for a few well-known protocols.
String protocol = ds.getLocator().getProtocol();
if (protocol != null) {
protocol = protocol.toLowerCase();
if (protocol.equals("file") ||
protocol.startsWith("http") ||
protocol.equals("ftp")) {
useMoreRenderBuffer = true;
}
}
}
jmd = new BasicJMD(jmdTitle);
}
if (DEBUG) source.setJMD(jmd);
source.setController(this);
dsource = ds;
// If it's RTP data source, we'll disable prefetch and reset.
if (dsource instanceof com.sun.media.protocol.Streamable &&
!((com.sun.media.protocol.Streamable)dsource).isPrefetchable()) {
prefetchEnabled = false;
dataPathBlocked = true;
}
if (dsource instanceof CaptureDevice)
prefetchEnabled = false;
}
String configError = "Failed to configure: " + this;
String configIntError = " The configure process is being interrupted.\n";
String configInt2Error = "interrupted while the Processor is being configured.";
String parseError = "failed to parse the input media.";
/**
* Configuring the engine.
*/
protected boolean doConfigure() {
if (!doConfigure1())
return false;
// The indices to the connector names, tracks, and track controls
// should all correspond to each other.
String names[] = source.getOutputConnectorNames();
trackControls = new BasicTrackControl[tracks.length];
for (int i = 0; i < tracks.length; i++) {
trackControls[i] = new PlayerTControl(this, tracks[i],
source.getOutputConnector(names[i]));
}
return doConfigure2();
}
/**
* Configure - Part I.
*/
protected boolean doConfigure1() {
long parsingTime = System.currentTimeMillis();
modules = new Vector();
filters = new Vector();
sinks = new Vector();
waitPrefetched = new Vector();
waitStopped = new Vector();
waitEnded = new Vector();
waitResetted = new Vector();
source.setModuleListener(this);
source.setController(this);
modules.addElement(source);
// Realize the source.
if (!source.doRealize()) {
Log.error(configError);
if (source.errMsg != null)
Log.error(" " + source.errMsg + "\n");
player.processError = parseError;
return false;
}
// Check for interrupt after a critical section.
if (isInterrupted()) {
Log.error(configError);
Log.error(configIntError);
player.processError = configInt2Error;
return false;
}
if ((parser = source.getDemultiplexer()) == null) {
Log.error(configError);
Log.error(" Cannot obtain demultiplexer for the source.\n");
player.processError = parseError;
return false;
}
// Build the track controls.
try {
tracks = parser.getTracks();
} catch (Exception e) {
Log.error(configError);
Log.error(" Cannot obtain tracks from the demultiplexer: " + e + "\n");
player.processError = parseError;
return false;
}
// Check for interrupt after a critical section.
if (isInterrupted()) {
Log.error(configError);
Log.error(configIntError);
player.processError = configInt2Error;
return false;
}
profile("parsing", parsingTime);
return true;
}
/**
* Configure - Part II.
*/
protected boolean doConfigure2() {
if (parser.isPositionable() && parser.isRandomAccess()) {
Track master = FramePositioningAdapter.getMasterTrack(tracks);
if (master != null) {
framePositioningControl = new FramePositioningAdapter(player, master);
}
}
return true;
}
protected String realizeError = "Failed to realize: " + this;
protected String timeBaseError = " Cannot manage the different time bases.\n";
protected String genericProcessorError = "cannot handle the customized options set on the Processor.\nCheck jmf.log for full details.";
/**
* @return true if successful.
*/
protected synchronized boolean doRealize() {
return doRealize1() && doRealize2();
}
/**
* doRealize Part I
*/
protected boolean doRealize1() {
Log.comment("Building flow graph for: " + dsource.getLocator() + "\n");
realizeTime = System.currentTimeMillis();
boolean atLeastOneTrack = false;
int trackID = 0;
int numTracks = getNumTracks();
// Go thru each track to build the graph.
for (int i = 0; i < trackControls.length; i++) {
// Skip the track if its disabled
if (!trackControls[i].isEnabled())
continue;
Log.setIndent(0);
Log.comment("Building Track: " + i);
if (trackControls[i].buildTrack(trackID, numTracks)) {
// We've completed at least one track now.
atLeastOneTrack = true;
trackControls[i].setEnabled(true);
} else if (trackControls[i].isCustomized()) {
// If the track is being customized and we cannot
// build a graph for that track, we won't attempt to
// build the rest of the tracks.
Log.error(realizeError);
trackControls[i].prError();
player.processError = genericProcessorError;
return false;
} else {
// Disable that track
trackControls[i].setEnabled(false);
Log.warning("Failed to handle track " + i);
trackControls[i].prError();
}
// Check for interrupt after a critcial section.
if (isInterrupted()) {
Log.error(realizeError);
Log.error(" The graph building process is being interrupted.\n");
player.processError = "interrupted while the player is being constructed.";
return false;
}
trackID++;
Log.write("\n");
}
// Failed if none of the tracks worked.
if (!atLeastOneTrack) {
Log.error(realizeError);
player.processError = "input media not supported: " + getCodecList();
return false;
}
return true;
}
/**
* doRealize Part II
*/
protected boolean doRealize2() {
// Locate the set the master time base.
if (!manageTimeBases()) {
Log.error(realizeError);
Log.error(timeBaseError);
player.processError = timeBaseError;
return false;
}
// For debugging
Log.comment("Here's the completed flow graph:");
traceGraph(source);
Log.write("\n");
profile("graph building", realizeTime);
realizeTime = System.currentTimeMillis();
// Update the format info on the progress control.
updateFormats();
// For Java Media Debugger
if (DEBUG) jmd.initGraph(source);
profile("realize, post graph building", realizeTime);
return true;
}
String getCodecList() {
String list = "";
Format fmt;
for (int i = 0; i < trackControls.length; i++) {
fmt = trackControls[i].getOriginalFormat();
if (fmt == null || fmt.getEncoding() == null)
continue;
list += fmt.getEncoding();
if (fmt instanceof VideoFormat)
list += " video";
else if (fmt instanceof AudioFormat)
list += " audio";
if (i + 1 < trackControls.length)
list += ", ";
}
return list;
}
int getNumTracks() {
int num = 0;
for (int i = 0; i < trackControls.length; i++) {
if (trackControls[i].isEnabled())
num++;
}
return num;
}
/**
* Search and update the master time base.
*/
boolean manageTimeBases() {
// Obtain a master time base from one of its SinkModules.
masterSink = findMasterSink();
return updateMasterTimeBase();
}
/**
* Returns the DataSink which holds the timebase.
*/
protected BasicSinkModule findMasterSink() {
// Search for all the enabled track for a master time base.
for (int i = 0; i < trackControls.length; i++) {
if (!trackControls[i].isEnabled())
continue;
if (trackControls[i].rendererModule != null &&
trackControls[i].rendererModule.getClock() != null) {
return trackControls[i].rendererModule;
}
}
return null;
}
/**
* Update the master timebase on all the modules.
*/
boolean updateMasterTimeBase() {
BasicSinkModule bsm;
int size = sinks.size();
if (masterSink != null)
slaveClock.setMaster(masterSink.getClock());
else
// No master sink found in the module, we'll use the
// system time base.
slaveClock.setMaster(null);
// Set the master time base to each of the SinkModules.
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
if (bsm != masterSink && !bsm.prefetchFailed()) {
try {
bsm.setTimeBase(slaveClock.getTimeBase());
} catch (IncompatibleTimeBaseException e) {
return false;
}
}
}
return true;
}
/**
* Called when doConfigure() is aborted.
*/
protected synchronized void abortConfigure() {
// The parser could be in the middle of realizing. We'll need to
// abort it.
if (source != null)
source.abortRealize();
}
/**
* Called when the realize() is aborted, i.e. deallocate() was called
* while realizing. Release all resources claimed previously by the
* realize() call.
*/
protected synchronized void abortRealize() {
StateTransistor m;
int size = modules.size();
for (int i = 0; i < size; i++) {
m = (StateTransistor)modules.elementAt(i);
m.abortRealize();
}
}
/**
* Called when realize() has failed.
*/
protected synchronized void doFailedRealize() {
StateTransistor m;
int size = modules.size();
for (int i = 0; i < size; i++) {
m = (StateTransistor)modules.elementAt(i);
m.doFailedRealize();
}
super.doFailedRealize();
}
String prefetchError = "Failed to prefetch: " + this;
/**
* The stub function to perform the steps to prefetch the controller.
* @return true if successful.
*/
protected synchronized boolean doPrefetch() {
if (prefetched)
return true;
return doPrefetch1() && doPrefetch2();
}
/**
* doPrefetch - Part I
*/
protected boolean doPrefetch1() {
// If the engine was deallocated before this prefetch, we'll
// need to sync up the media at the time when the prefetch
// happens.
if (timeBeforeAbortPrefetch != null) {
doSetMediaTime(timeBeforeAbortPrefetch);
timeBeforeAbortPrefetch = null;
}
prefetchTime = System.currentTimeMillis();
// Then prefetch the data path by starting the source
// module without starting the renderer modules.
resetPrefetchedList();
// First prefetch each modules.
// Fail if source cannot be prefetched.
if (!source.doPrefetch()) {
Log.error(prefetchError);
if (dsource != null)
Log.error(" Cannot prefetch the source: " + dsource.getLocator() + "\n");
return false;
}
// Prefetch each track.
boolean atLeastOneTrack = false;
boolean usedToFailed;
for (int i = 0; i < trackControls.length; i++) {
usedToFailed = trackControls[i].prefetchFailed;
// If this track used to fail and we haven't been
// deallocated, then we just skip the track.
if (usedToFailed && getState() > Prefetching)
continue;
if (trackControls[i].prefetchTrack()) {
atLeastOneTrack = true;
if (usedToFailed) {
if (!manageTimeBases()) {
Log.error(prefetchError);
Log.error(timeBaseError);
return false;
}
// Sync up all the tracks with the current media time
// since a new track is added.
doSetMediaTime(getMediaTime());
}
} else {
trackControls[i].prError();
// If the failed track contain the time base,
// we'll need to choose a new time base.
if (trackControls[i].isTimeBase()) {
// Fails if no new time base can be found.
// This should be rare.
if (!manageTimeBases()) {
Log.error(prefetchError);
Log.error(timeBaseError);
player.processError = timeBaseError;
return false;
}
}
if (trackControls[i].getFormat() instanceof AudioFormat &&
trackControls[i].rendererFailed) {
player.processError = "cannot open the audio device.";
}
}
}
// Fail if not even one track can be prefetched.
if (!atLeastOneTrack) {
Log.error(prefetchError);
return false;
}
player.processError = null;
return true;
}
/**
* doPrefetch - Part II
*/
protected boolean doPrefetch2() {
if (prefetchEnabled) {
// Wait for notification from the renderer modules with the
// bufferPrefetched event.
synchronized (waitPrefetched) {
source.doStart();
try {
if (!waitPrefetched.isEmpty()) {
// Block for at most 3 sec. in case anything goes wrong
// at the renderer modules.
waitPrefetched.wait(3000);
}
} catch (InterruptedException e) {}
}
} else
prefetched = true;
deallocated = false;
return true;
}
/**
* Called when the prefetch() is aborted, i.e. deallocate() was called
* while prefetching. Release all resources claimed previously by the
* prefetch call.
* Override this to implement subclass behavior.
*/
protected synchronized void abortPrefetch() {
// Mark the time before the prefetch. At the next prefetch,
// we'll sync up the media with this time.
timeBeforeAbortPrefetch = getMediaTime();
doReset();
StateTransistor m;
int size = modules.size();
for (int i = 0; i < size; i++) {
m = (StateTransistor)modules.elementAt(i);
m.abortPrefetch();
}
deallocated = true;
}
/**
* Called when the prefetch() has failed.
*/
protected synchronized void doFailedPrefetch() {
StateTransistor m;
int size = modules.size();
for (int i = 0; i < size; i++) {
m = (StateTransistor)modules.elementAt(i);
m.doFailedPrefetch();
}
super.doFailedPrefetch();
}
/**
* Start immediately.
* Invoked from start(tbt) when the scheduled start time is reached.
* Use the public start(tbt) method for the public interface.
* Override this to implement subclass behavior.
*/
protected synchronized void doStart() {
if (started) return;
doStart1();
doStart2();
}
/**
* doStart - Part I.
*/
protected void doStart1() {
// If the data source is a capture device, we would like
// to flush the data source before starting.
if (dsource instanceof CaptureDevice || isRTP())
reset();
resetPrefetchedList();
resetStoppedList();
resetEndedList();
for (int i = 0; i < trackControls.length; i++) {
if (trackControls[i].isEnabled())
trackControls[i].startTrack();
}
}
/**
* doStart - Part II
*/
protected void doStart2() {
source.doStart();
started = true;
prefetched = true;
}
/**
* This is stop by request.
*/
public synchronized void stop() {
super.stop();
sendEvent(new StopByRequestEvent(this, Started,
Prefetched,
getTargetState(),
getMediaTime()));
}
protected synchronized void localStop() {
super.stop();
}
/**
* Invoked from stop().
* Override this to implement subclass behavior.
*/
protected synchronized void doStop() {
if (!started) return;
doStop1();
doStop2();
}
/**
* doStop - Part I.
*/
protected void doStop1() {
resetPrefetchedList();
source.doStop();
for (int i = 0; i < trackControls.length; i++) {
if (trackControls[i].isEnabled())
trackControls[i].stopTrack();
}
}
/**
* doStop - Part II.
*/
protected void doStop2() {
// If prefetching is disabled, we'll also need to do a
// non-blocking stop on the source since stop is normally
// called when data is prefetched.
if (!prefetchEnabled)
source.pause();
started = false;
}
/**
* Override BasicController.setStopTime to allow for more
* accurate stop time set.
*/
public void setStopTime(Time t) {
if (getState() < Realized)
throwError(new NotRealizedError("Cannot set stop time on an unrealized controller."));
if ( getStopTime() != null &&
getStopTime().getNanoseconds() != t.getNanoseconds() )
sendEvent(new StopTimeChangeEvent(this, t));
if (getState() == Started &&
t != Clock.RESET && t.getNanoseconds() < getMediaNanoseconds()) {
// We have already passed the stop time.
localStop();
setStopTime(Clock.RESET);
sendEvent(new StopAtTimeEvent(this, getState(),
Prefetched,
getTargetState(),
getMediaTime()));
} else {
getClock().setStopTime(t);
int size = sinks.size();
BasicSinkModule bsm;
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
bsm.setStopTime(t);
}
}
}
/**
* Called by deallocate().
* Subclasses should implement this for its specific behavior.
*/
protected void doDeallocate() {
}
/**
* Invoked by close() to cleanup the Controller.
* Override this to implement subclass behavior.
*/
protected synchronized void doClose() {
if (modules == null) {
if (source != null)
source.doClose();
return;
}
if (getState() == Started)
localStop();
if (getState() == Prefetched)
doReset();
StateTransistor m;
int size = modules.size();
for (int i = 0; i < size; i++) {
m = (StateTransistor)modules.elementAt(i);
m.doClose();
}
if (needSavingDB) {
Resource.saveDB();
needSavingDB = false;
}
}
RTPInfo rtpInfo = null;
boolean testedRTP = false;
public boolean isRTP() {
if (testedRTP)
return rtpInfo != null;
rtpInfo = (RTPInfo)dsource.getControl("com.sun.media.util.RTPInfo");
testedRTP = true;
return rtpInfo != null;
}
public String getCNAME() {
if (rtpInfo == null) {
if ((rtpInfo = (RTPInfo)dsource.getControl("com.sun.media.util.RTPInfo")) == null)
return null;
}
return rtpInfo.getCNAME();
}
/**
* Override BasicController's setMediaTime so as not to set the
* media time on the master clock twice.
*/
public synchronized void setMediaTime(Time when) {
if (state < Realized)
throwError(new NotRealizedError("Cannot set media time on a unrealized controller"));
// Chances of this are really rare. Nevertheless, we'll guard
// against it.
if (when.getNanoseconds() == getMediaNanoseconds())
return;
// Reset everything.
reset();
// In case deallocate was called, we need to
// clear the timeBeforeAbortPrefetch flag since we are
// setting a new time.
timeBeforeAbortPrefetch = null;
doSetMediaTime(when);
// Prefetch the engine again to display the first frame.
doPrefetch();
sendEvent(new MediaTimeSetEvent(this, when));
}
protected void doSetMediaTime(Time when) {
slaveClock.setMediaTime(when);
// Set media time on the renderer modules and the source.
// Check the return time from the parser.
Time t;
if ((t = source.setPosition(when, 0)) == null)
t = when;
int size = sinks.size();
BasicSinkModule bsm;
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
bsm.doSetMediaTime(when);
bsm.setPreroll(when.getNanoseconds(), t.getNanoseconds());
}
}
public synchronized float doSetRate(float r) {
if (r <= 0f)
r = 1.0f;
if (r == rate)
return r;
// If the system clock is in use, we need to set rate
// on the system clock. Otherwise, set rate on the
// master clock and let it determine the actual rate use.
if (masterSink == null)
r = getClock().setRate(r);
else
r = masterSink.doSetRate(r);
BasicModule m;
int size = modules.size();
for (int i = 0; i < size; i++) {
m = (BasicModule)modules.elementAt(i);
if (m != masterSink)
m.doSetRate(r);
}
rate = r;
return r;
}
/**
* Flush (reset) the flow graph.
* This is a blocking call and should only be called when the
* engine is stopped.
*/
protected synchronized void reset() {
// If a module has been blocked, we'll need
// to return from the reset.
if (started || !prefetched || dataPathBlocked)
return;
doReset();
}
/**
* The real reset code.
* This is a blocking call and should only be called when the
* engine is stopped.
*/
protected synchronized void doReset() {
// We'll need to block until the modules are fully resetted to
// move on.
synchronized (waitResetted) {
resetResettedList();
// Put all modules in the reset state. The source module
// is the last to receive the reset call. It will trigger
// zero-length flush buffers to flow from the source to
// the sinks. When the sinks receive the flush buffers,
// it will reply back with the resetted callbacks.
BasicModule m;
int size = modules.size();
for (int i = size-1; i >= 0; i--) {
m = (BasicModule)modules.elementAt(i);
if (!m.prefetchFailed())
m.reset();
}
// The data flow (including the flush buffers) could be blocked
// at the sinks. To initiate the reset, we'll call
// triggerReset on the sinks.
BasicSinkModule bsm;
size = sinks.size();
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
if (!bsm.prefetchFailed())
bsm.triggerReset();
}
if (!waitResetted.isEmpty()) {
try {
// Wait at most 3 seconds in case anything goes
// wrong at the plugins. We have no control on
// how plugin writers write their plugin's.
waitResetted.wait(3000);
} catch (Exception e) {}
}
// Some of the sinks may need to be stop again.
size = sinks.size();
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
if (!bsm.prefetchFailed())
bsm.doneReset();
}
}
prefetched = false;
}
/**
* Reset the renderer lists.
*/
private void resetPrefetchedList() {
synchronized (waitPrefetched) {
waitPrefetched.removeAllElements();
int size = sinks.size();
BasicSinkModule bsm;
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
if (!bsm.prefetchFailed())
waitPrefetched.addElement(bsm);
}
waitPrefetched.notifyAll();
}
}
/**
* Reset the renderer lists.
*/
private void resetStoppedList() {
synchronized (waitStopped) {
waitStopped.removeAllElements();
int size = sinks.size();
BasicSinkModule bsm;
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
if (!bsm.prefetchFailed())
waitStopped.addElement(bsm);
}
waitStopped.notifyAll();
}
}
/**
* Reset the renderer lists.
*/
private void resetEndedList() {
synchronized (waitEnded) {
waitEnded.removeAllElements();
int size = sinks.size();
BasicSinkModule bsm;
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
if (!bsm.prefetchFailed())
waitEnded.addElement(bsm);
}
waitEnded.notifyAll();
}
}
/**
* Reset the renderer lists.
*/
private void resetResettedList() {
// Set up the wait queue for the reset notifications first.
// We'll wait for reset note from sinks and the source only.
synchronized (waitResetted) {
waitResetted.removeAllElements();
int size = sinks.size();
BasicSinkModule bsm;
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
if (!bsm.prefetchFailed())
waitResetted.addElement(bsm);
}
waitResetted.notifyAll();
}
}
boolean prefetchLogged = false;
public void bufferPrefetched(Module src) {
if (!prefetchEnabled)
return;
if (src instanceof BasicSinkModule) {
synchronized (waitPrefetched) {
if (waitPrefetched.contains(src))
waitPrefetched.removeElement(src);
if (waitPrefetched.isEmpty()) {
// All sinks are prefetched. Wake up the
// doPrefetch() wait.
waitPrefetched.notifyAll();
if (!prefetchLogged) {
profile("prefetch", prefetchTime);
prefetchLogged = true;
}
if (getState() != Controller.Started &&
getTargetState() != Controller.Started) {
// Non-blocking stop.
source.pause();
}
prefetched = true;
}
}
}
}
public void stopAtTime(Module src) {
if (src instanceof BasicSinkModule) {
synchronized (waitStopped) {
if (waitStopped.contains(src))
waitStopped.removeElement(src);
// Check to see if all the tracks have stopped or
// all other tracks have reached EOM and this is the
// only track left behind.
if (waitStopped.isEmpty() ||
waitEnded.size() == 1 && waitEnded.contains(src)) {
// Stop at time.
started = false;
stopControllerOnly();
setStopTime(Clock.RESET);
sendEvent(new StopAtTimeEvent(this, Started,
Prefetched,
getTargetState(),
getMediaTime()));
slaveClock.reset(USE_MASTER);
} else if (src == masterSink) {
// If the master sink has finished but the
// not all the other tracks are finished, we'll
// need to switch the clock to using a backup for
// the rest of the media.
slaveClock.reset(USE_BACKUP);
}
}
}
}
public void mediaEnded(Module src) {
if (src instanceof BasicSinkModule) {
synchronized (waitEnded) {
if (waitEnded.contains(src))
waitEnded.removeElement(src);
if (waitEnded.isEmpty()) {
// EOM.
started = false;
stopControllerOnly();
sendEvent(new EndOfMediaEvent(this, Started,
Prefetched, getTargetState(),
getMediaTime()));
slaveClock.reset(USE_MASTER);
} else if (src == masterSink) {
// If the master sink has finished but the
// not all the other tracks are finished, we'll
// need to switch the clock to using a backup for
// the rest of the media.
slaveClock.reset(USE_BACKUP);
}
}
}
}
public void resetted(Module src) {
synchronized (waitResetted) {
if (waitResetted.contains(src)) {
waitResetted.removeElement(src);
}
if (waitResetted.isEmpty()) {
// Everyone modules has been resetted.
waitResetted.notifyAll();
}
}
}
public void dataBlocked(Module src, boolean blocked) {
dataPathBlocked = blocked;
if (blocked) {
// Break the lock for the blocking prefetch & reset.
resetPrefetchedList();
resetResettedList();
}
if ( getTargetState() != Controller.Started) {
return;
}
if (blocked) {
localStop();
setTargetState(Controller.Started); // NOTE: may cause race condition
sendEvent( new RestartingEvent(this,
Controller.Started,
Controller.Prefetching,
Controller.Started,
getMediaTime()));
} else {
sendEvent( new StartEvent(this,
Controller.Prefetched,
Controller.Started,
Controller.Started,
getMediaTime(),
getTimeBase().getTime()));
}
}
public void framesBehind(Module src, float frames, InputConnector ic) {
OutputConnector oc;
BasicFilterModule bfm;
while (ic != null) {
if ((oc = ic.getOutputConnector()) == null)
break;
if ((src = oc.getModule()) == null)
break;
if (!(src instanceof BasicFilterModule))
break;
bfm = (BasicFilterModule)src;
bfm.setFramesBehind(frames);
ic = src.getInputConnector(null);
}
}
long markedDataStartTime = 0;
boolean reportOnce = false;
public void markedDataArrived(Module src, Buffer buffer) {
if (src instanceof BasicSourceModule) {
markedDataStartTime = getMediaNanoseconds();
} else {
long t = getMediaNanoseconds() - markedDataStartTime;
if (t > 0 && t < 1000000000) {
if (!reportOnce) {
Log.comment("Computed latency for video: " + t/1000000 + " ms\n");
reportOnce = true;
}
latency = (t + latency)/2;
}
}
}
public void formatChanged(Module src, Format oldFormat, Format newFormat) {
Log.comment(src + ": input format changed: " + newFormat);
// Handle size change independently.
if (src instanceof BasicRendererModule &&
oldFormat instanceof VideoFormat &&
newFormat instanceof VideoFormat) {
Dimension s1 = ((VideoFormat)oldFormat).getSize();
Dimension s2 = ((VideoFormat)newFormat).getSize();
if (s2 != null && (s1 == null || !s1.equals(s2))) {
sendEvent(new SizeChangeEvent(this, s2.width, s2.height, 1.0f));
}
}
}
public void formatChangedFailure(Module src, Format oldFormat, Format newFormat) {
// Send an internal error event for now.
// Future: need to rebuild the graph.
// -ivg
if (!internalErrorOccurred) {
sendEvent(new InternalErrorEvent(this, "Internal module " + src + ": failed to handle a data format change!"));
internalErrorOccurred = true;
close();
}
}
public void pluginTerminated(Module src) {
if (!internalErrorOccurred) {
sendEvent(new ControllerClosedEvent(this));
internalErrorOccurred = true;
close();
}
}
public void internalErrorOccurred(Module src) {
if (!internalErrorOccurred) {
sendEvent(new InternalErrorEvent(this, "Internal module " + src + " failed!"));
internalErrorOccurred = true;
close();
}
}
/**
* Return true if audio is present.
*/
public boolean audioEnabled() {
for (int i = 0; i < trackControls.length; i++) {
if (trackControls[i].isEnabled() &&
trackControls[i].getOriginalFormat() instanceof AudioFormat)
return true;
}
return false;
}
/**
* Return true if video is present.
*/
public boolean videoEnabled() {
for (int i = 0; i < trackControls.length; i++) {
if (trackControls[i].isEnabled() &&
trackControls[i].getOriginalFormat() instanceof VideoFormat)
return true;
}
return false;
}
/**
* Return a list of <b>Control</b> objects this <b>Controller</b>
* supports.
* In this case, it is all the controls from all the modules
* controlled by this engine.
*
* @return list of <b>Controller</b> controls.
*/
public Control[] getControls() {
// build the list of controls. It is the total of all the
// controls from each module plus the ones that are maintained
// by the engine itself (e.g. progressControl).
Control controls[];
Vector cv = new Vector();
Control c;
Object cs[];
Module m;
int i, size = (modules == null ? 0 : modules.size());
int otherSize = 0;
// Collect controls from all the modules.
for (i = 0; i < size; i++) {
m = (Module)modules.elementAt(i);
cs = m.getControls();
if (cs == null) continue;
for (int j = 0; j < cs.length; j++) {
cv.addElement(cs[j]);
}
}
size = cv.size();
// Controls owned by the engine itself.
if (videoEnabled()) {
if (frameRateControl == null) {
frameRateControl = new FrameRateAdapter(player, 0f, 0f, 30f, false) {
public float setFrameRate(float rate) {
this.value = rate;
return -1f;
}
public Component getControlComponent() {
return null;
}
public Object getOwner() {
return player;
}
};
}
}
if (bitRateControl == null) {
bitRateControl = new BitRateA(0, -1, -1, false);
}
if (frameRateControl != null)
otherSize++;
if (bitRateControl != null)
otherSize++;
if (framePositioningControl != null)
otherSize++;
if (DEBUG) otherSize++;
controls = new Control[size + otherSize + trackControls.length];
for (i = 0; i < size; i++)
controls[i] = (Control)cv.elementAt(i);
if (bitRateControl != null)
controls[size++] = bitRateControl;
if (frameRateControl != null)
controls[size++] = frameRateControl;
if (framePositioningControl != null)
controls[size++] = framePositioningControl;
if (DEBUG)
controls[size++] = jmd;
// and the tracks
for (i = 0; i < trackControls.length; i++) {
controls[size + i] = trackControls[i];
}
return controls;
}
/**
* Get audio gain control.
*/
public GainControl getGainControl() {
return (GainControl)getControl("javax.media.GainControl");
}
/**
* Get the visual component where the video is presented.
*/
public Component getVisualComponent() {
Vector visuals = new Vector(1);
if (modules == null)
return null;
for (int i = 0; i < modules.size(); i++) {
BasicModule bm = (BasicModule) modules.elementAt(i);
PlugIn pi = getPlugIn(bm);
if (pi instanceof VideoRenderer) {
Component comp = ((VideoRenderer)pi).getComponent();
if (comp != null)
visuals.addElement(comp);
}
}
if (visuals.size() == 0)
return null;
else if (visuals.size() == 1)
return (Component) visuals.elementAt(0);
else {
// Multiple components, put them into one container
return createVisualContainer(visuals);
}
}
protected Component createVisualContainer(Vector visuals) {
Boolean hint = (Boolean) Manager.getHint(Manager.LIGHTWEIGHT_RENDERER);
if (container == null) {
if (hint == null || hint.booleanValue() == false) {
container = new HeavyPanel(visuals);
} else {
container = new LightPanel(visuals);
}
container.setLayout( new FlowLayout() );
container.setBackground(Color.black);
for (int i = 0; i < visuals.size(); i++) {
Component c = (Component)visuals.elementAt(i);
container.add(c);
c.setSize(c.getPreferredSize());
}
}
return container;
}
/**
* Returns the start latency.
* Don't know until the particular node is implemented.
* @return the start latency.
*/
public Time getStartLatency() {
if ( (state == Unrealized) || (state == Realizing) )
throwError(new NotRealizedError("Cannot get start latency from an unrealized controller"));
return LATENCY_UNKNOWN;
}
/**
* Return the run-time latency. It's the time it takes for a packet
* to travel from the first module to the last module.
* The time is computed in nanoseconds.
*/
public long getLatency() {
return latency;
}
/**
* Return the duration of the media.
* It's unknown until we implement a particular node.
* @return the duration of the media.
*/
public Time getDuration() {
return source.getDuration();
}
public void setProgressControl(ProgressControl p){
progressControl = p;
}
/**
* Create the progress status control.
*/
public void createProgressControl() {
// Build the progress control.
StringControl frameRate;
StringControl bitRate;
StringControl videoProps;
StringControl audioProps;
StringControl videoCodec;
StringControl audioCodec;
frameRate = new StringControlAdapter();
frameRate.setValue(JMFI18N.getResource("mediaplayer.N/A"));
bitRate = new StringControlAdapter();
bitRate.setValue(JMFI18N.getResource("mediaplayer.N/A"));
videoProps = new StringControlAdapter();
videoProps.setValue(JMFI18N.getResource("mediaplayer.N/A"));
audioProps = new StringControlAdapter();
audioProps.setValue(JMFI18N.getResource("mediaplayer.N/A"));
audioCodec = new StringControlAdapter();
audioCodec.setValue(JMFI18N.getResource("mediaplayer.N/A"));
videoCodec = new StringControlAdapter();
videoCodec.setValue(JMFI18N.getResource("mediaplayer.N/A"));
progressControl = new ProgressControlAdapter( frameRate,
bitRate,
videoProps,
audioProps,
videoCodec,
audioCodec );
}
/**
* Update the format info per track on the progress control.
*/
public void updateFormats() {
// Update formats per track.
for (int i = 0; i < trackControls.length; i++) {
trackControls[i].updateFormat();
}
}
long lastBitRate = 0;
long lastStatsTime = 0; // so now - lastStatsTime will not be 0.
/**
* Update the aggregate bit rate and frame rate per track on
* the progress control.
*/
public void updateRates() {
if (getState() < Realized)
return;
// Update global bit rate.
long now = System.currentTimeMillis();
long rate, avg;
if (now == lastStatsTime)
rate = lastBitRate;
else
rate = (long)((double)getBitRate()*8.0/
(now - lastStatsTime)*1000.0);
avg = (lastBitRate + rate)/2;
if (bitRateControl != null) {
bitRateControl.setBitRate((int)avg);
}
lastBitRate = rate;
lastStatsTime = now;
resetBitRate();
// Update frame rate on a per-track basis.
for (int i = 0; i < trackControls.length; i++) {
trackControls[i].updateRates(now);
}
// Compute the instanteous latency.
source.checkLatency();
}
protected long getBitRate() {
return source.getBitsRead();
}
protected void resetBitRate() {
source.resetBitsRead();
}
/**
* Override the parent's method to not check for realized state. There's
* no need to.
*/
public void setTimeBase(TimeBase tb) throws IncompatibleTimeBaseException {
getClock().setTimeBase(tb);
// Set the time base on each of the SinkModules.
if (sinks == null)
return;
int size = sinks.size();
BasicSinkModule bsm;
for (int i = 0; i < size; i++) {
bsm = (BasicSinkModule)sinks.elementAt(i);
bsm.setTimeBase(tb);
}
}
/**
* Override the parent's method to not check for realized state. There's
* no need to.
*/
public TimeBase getTimeBase() {
return getClock().getTimeBase();
}
//////////////////////////////////
//
// Flow graph building routines.
//////////////////////////////////
/**
* Construct a track (connected modules) from the specified node graph.
* Return a non-null GraphNode if the track cannot be built. The
* non-null node returned is the node that failed to be opened.
*/
protected GraphNode buildTrackFromGraph(BasicTrackControl tc,
GraphNode node) {
BasicModule src = null, dst = null;
InputConnector ic = null;
OutputConnector oc = null;
boolean lastNode = true;
Vector used = new Vector(5);
int indent = 0;
if (node.plugin == null) {
// There's nothing to build.
// i.e. the output from the source (demux) works just fine.
// Probably just need to be multiplexed.
return null;
}
Log.setIndent(indent++);
// Build the graph from the last node.
while (node != null && node.plugin != null) {
if ((src = createModule(node, used)) == null) {
// something terrible went wrong.
Log.error("Internal error: buildTrackFromGraph");
node.failed = true;
return node;
}
// Mark the renderer module or the last output connector
// from in the trackcontrol.
if (lastNode) {
if (src instanceof BasicRendererModule) {
tc.rendererModule = (BasicRendererModule)src;
// If we are dealing with an audio renderer here, we'll
// set the jitter buffer size.
if (useMoreRenderBuffer &&
tc.rendererModule.getRenderer() instanceof AudioRenderer)
setRenderBufferSize(tc.rendererModule.getRenderer());
} else if (src instanceof BasicFilterModule) {
tc.lastOC = src.getOutputConnector(null);
tc.lastOC.setFormat(node.output);
}
lastNode = false;
}
ic = src.getInputConnector(null);
ic.setFormat(node.input);
if (dst != null) {
oc = src.getOutputConnector(null);
ic = dst.getInputConnector(null);
oc.setFormat(ic.getFormat());
}
src.setController(this);
if (!src.doRealize()) {
//Log.write("Failed to open plugin: " + node.plugin);
//Log.write(" with input: " + node.input);
Log.setIndent(indent--);
node.failed = true;
return node;
}
if (oc != null && ic != null)
connectModules(oc, ic, dst);
dst = src;
node = node.prev;
}
// All successful, so we need to add each modules under the engine's
// management.
dst = src;
while (true) {
dst.setModuleListener(this);
modules.addElement(dst);
tc.modules.addElement(dst);
if (dst instanceof BasicFilterModule)
filters.addElement(dst);
else if (dst instanceof BasicSinkModule)
sinks.addElement(dst);
oc = dst.getOutputConnector(null);
if (oc == null ||
(ic = oc.getInputConnector()) == null ||
(dst = (BasicModule)ic.getModule()) == null)
break;
}
// Set the first output connector.
tc.firstOC.setFormat(tc.getOriginalFormat());
// Check if the first module has the format set.
ic = src.getInputConnector(null);
Format fmt = ic.getFormat();
if (fmt == null || !fmt.equals(tc.getOriginalFormat()))
ic.setFormat(tc.getOriginalFormat());
connectModules(tc.firstOC, ic, src);
Log.setIndent(indent--);
return null;
}
protected void setRenderBufferSize(Renderer r) {
BufferControl bc = (BufferControl)r.getControl("javax.media.control.BufferControl");
if (bc != null)
bc.setBufferLength(2000);
}
/**
* Given a chain of FilterModules, return the last one of the chain.
*/
protected BasicModule lastModule(BasicModule bm) {
OutputConnector oc;
InputConnector ic;
oc = bm.getOutputConnector(null);
while (oc != null && (ic = oc.getInputConnector()) != null) {
bm = (BasicModule)ic.getModule();
oc = bm.getOutputConnector(null);
}
return bm;
}
/**
* Create a realized filter module given the plugIn codec.
*/
protected BasicModule createModule(GraphNode n, Vector used) {
PlugIn p;
BasicModule m = null;
if (n.plugin == null)
return null;
if (used.contains(n.plugin)) {
// That plugin has already been used in the same path,
// we'll need to instantiate another one of its kind.
if (n.cname == null ||
(p = SimpleGraphBuilder.createPlugIn(n.cname, -1)) == null) {
Log.write("Failed to instantiate " + n.cname);
return null;
}
} else {
p = n.plugin;
used.addElement(p);
}
if ((n.type == -1 || n.type == PlugInManager.RENDERER) &&
p instanceof Renderer) {
m = new BasicRendererModule((Renderer)p);
} else if ((n.type == -1 || n.type == PlugInManager.CODEC) &&
p instanceof Codec) {
m = new BasicFilterModule((Codec)p);
}
if (DEBUG && m != null) m.setJMD(jmd);
return m;
}
/**
* Connect the two given modules.
*/
protected void connectModules(OutputConnector oc, InputConnector ic, BasicModule dst) {
// If the dst is the renderer, adopt the dest's protocol.
// Otherwise, adopt the source's protocol.
if (dst instanceof BasicRendererModule)
oc.setProtocol(ic.getProtocol());
else
ic.setProtocol(oc.getProtocol());
oc.connectTo(ic, ic.getFormat());
}
/**
* Return true if the given format is a raw video format.
*/
static boolean isRawVideo(Format fmt) {
return (fmt instanceof RGBFormat || fmt instanceof YUVFormat);
}
/**
* Trace the flow graph for debugging.
*/
void traceGraph(BasicModule source) {
Module m;
OutputConnector oc;
InputConnector ic;
String names[];
names = source.getOutputConnectorNames();
for (int i = 0; i < names.length; i++) {
oc = source.getOutputConnector(names[i]);
if ((ic = oc.getInputConnector()) == null)
continue;
if ((m = ic.getModule()) == null)
continue;
Log.write(" " + getPlugIn(source));
Log.write(" connects to: " + getPlugIn((BasicModule)m));
Log.write(" format: " + oc.getFormat());
traceGraph((BasicModule)m);
}
}
/**
* Get the plugin from a module. For debugging.
*/
protected PlugIn getPlugIn(BasicModule m) {
if (m instanceof BasicSourceModule)
return ((BasicSourceModule)m).getDemultiplexer();
if (m instanceof BasicFilterModule)
return ((BasicFilterModule)m).getCodec();
if (m instanceof BasicRendererModule)
return ((BasicRendererModule)m).getRenderer();
return null;
}
static void profile(String msg, long time) {
Log.profile("Profile: " + msg + ": " +
(System.currentTimeMillis() - time) + " ms\n");
}
//////////////////////////////////
//
// Inner classes
//////////////////////////////////
class BitRateA extends BitRateAdapter implements Owned {
public BitRateA(int initialBitRate, int minBitRate, int maxBitRate,
boolean settable) {
super(initialBitRate, minBitRate, maxBitRate, settable);
}
public int setBitRate(int rate) {
this.value = rate;
return this.value;
}
public Component getControlComponent() {
return null;
}
public Object getOwner() {
return player;
}
}
static boolean USE_MASTER = true;
static boolean USE_BACKUP = false;
class SlaveClock implements Clock {
Clock master, current;
BasicClock backup;
SlaveClock() {
backup = new BasicClock();
current = backup;
}
public void setMaster(Clock master) {
this.master = master;
current = (master == null ? backup : master);
if (master != null) {
try {
backup.setTimeBase(master.getTimeBase());
} catch (IncompatibleTimeBaseException e) {
}
}
}
public void setTimeBase(TimeBase tb) throws
IncompatibleTimeBaseException{
synchronized (backup) {
backup.setTimeBase(tb);
}
}
public void syncStart(Time tbt) {
synchronized (backup) {
if (backup.getState() != BasicClock.STARTED)
backup.syncStart(tbt);
}
}
public void stop() {
synchronized (backup) {
backup.stop();
}
}
public void setStopTime(Time t) {
synchronized (backup) {
backup.setStopTime(t);
}
}
public Time getStopTime() {
return backup.getStopTime();
}
public void setMediaTime(Time now) {
synchronized (backup) {
if (backup.getState() == BasicClock.STARTED) {
backup.stop();
backup.setMediaTime(now);
backup.syncStart(backup.getTimeBase().getTime());
} else
backup.setMediaTime(now);
}
}
public Time getMediaTime() {
return current.getMediaTime();
}
public long getMediaNanoseconds() {
return current.getMediaNanoseconds();
}
public Time getSyncTime() {
return current.getSyncTime();
}
public TimeBase getTimeBase() {
return current.getTimeBase();
}
public Time mapToTimeBase(Time t) throws ClockStoppedException {
return current.mapToTimeBase(t);
}
public float setRate(float factor) {
return backup.setRate(factor);
}
public float getRate() {
return current.getRate();
}
protected void reset(boolean useMaster) {
if (master != null && useMaster)
current = master;
else {
// Align the backup clock's media time with the
// master's time.
if (master != null) {
synchronized (backup) {
boolean started = false;
if (backup.getState() == BasicClock.STARTED) {
backup.stop();
started = true;
}
backup.setMediaTime(master.getMediaTime());
if (started)
backup.syncStart(backup.getTimeBase().getTime());
}
}
current = backup;
}
}
}
/**
* Track Control.
*/
class PlayerTControl extends BasicTrackControl implements Owned {
protected PlayerGraphBuilder gb;
public PlayerTControl(PlaybackEngine engine, Track track, OutputConnector oc) {
super(engine, track, oc);
}
public Object getOwner() {
return player;
}
/**
* Top level routine to build a single track.
*/
public boolean buildTrack(int trackID, int numTracks) {
if (gb == null)
gb = new PlayerGraphBuilder(engine);
else
gb.reset();
boolean rtn = gb.buildGraph(this);
// dispose the old GraphBuilder after building a track.
// The cache is not valid anymore.
gb = null;
return rtn;
}
/**
* Returns true if this track holds the master time base.
*/
public boolean isTimeBase() {
for (int j = 0; j < this.modules.size(); j++) {
if (this.modules.elementAt(j) == masterSink)
return true;
}
return false;
}
protected ProgressControl progressControl() {
return progressControl;
}
protected FrameRateControl frameRateControl() {
return frameRateControl;
}
}
/**
* This is the Graph builder extended from SimpleGraphBuilder
* to generate the data flow graph for the given track.
*/
class PlayerGraphBuilder extends SimpleGraphBuilder {
protected PlaybackEngine engine;
PlayerGraphBuilder(PlaybackEngine engine) {
this.engine = engine;
}
protected GraphNode buildTrackFromGraph(BasicTrackControl tc, GraphNode node) {
return engine.buildTrackFromGraph((PlayerTControl)tc, node);
}
}
class HeavyPanel extends java.awt.Panel implements VisualContainer {
public HeavyPanel(Vector visuals) {
}
}
class LightPanel extends java.awt.Container implements VisualContainer {
public LightPanel(Vector visuals) {
}
}
}
|