/*
* @(#)SimpleGraphBuilder.java 1.6 02/08/23
*
* Copyright (c) 1996-2002 Sun Microsystems, Inc. All rights reserved.
*/
package com.sun.media;
import java.util.*;
import javax.media.*;
import javax.media.format.*;
/**
*
* This is the Graph builder to generate the data flow graph for
* rendering an input format.
*
* It contains 3 parts:
* 1) Routines to search for all the supported output formats;
* 2) Routines to build a default flow graph -- buildGraph;
*
* A default graph is such that no customised option is specified on
* the TrackControl.
*
* It operates on a breath-first search algorithm until the final
* target is reached as defined by the findTarget() method.
* Intermediate search paths are stored as GraphNode's in the
* "candidates" vector.
*/
public class SimpleGraphBuilder {
// # of codec/converters allowed to use to complete a track.
protected int STAGES = 4;
protected Hashtable plugIns = new Hashtable(40);
protected GraphNode targetPlugins[] = null;
protected Vector targetPluginNames = null;
protected int targetType = -1;
int indent = 0;
// A non-published interface to trace the graph building process.
static protected GraphInspector inspector;
static public void setGraphInspector(GraphInspector insp) {
inspector = insp;
}
/**
* Reset local cache and reuse the same instance for graph building.
*/
public void reset() {
Enumeration enum = plugIns.elements();
GraphNode n;
while (enum.hasMoreElements()) {
n = (GraphNode)enum.nextElement();
n.resetAttempted();
}
}
/**
* Take a TrackControl and build the graph for it.
*/
boolean buildGraph(BasicTrackControl tc) {
Log.comment("Input: " + tc.getOriginalFormat());
Vector candidates = new Vector();
GraphNode node = new GraphNode(null, (PlugIn)null,
tc.getOriginalFormat(), null, 0);
indent = 1;
Log.setIndent(indent);
// Define the final targets.
if (!setDefaultTargets(tc.getOriginalFormat()))
return false;
candidates.addElement(node);
GraphNode failed;
while ((node = buildGraph(candidates)) != null) {
// Found a potential graph. Check if we can build a
// track from it.
if ((failed = buildTrackFromGraph(tc, node)) == null) {
// we are done.
indent = 0;
Log.setIndent(indent);
return true;
}
// If we can't build a track from it, it's because there's
// a node in the graph that cannot be opened. We'll have
// to reap it from the candidates and the registry.
removeFailure(candidates, failed, tc.getOriginalFormat() );
}
indent = 0;
Log.setIndent(indent);
return false;
}
/**
* When the graph build finds a viable graph to build, this callback
* will be invoked to see if the graph can actually be built.
* Subclass should implement this.
*/
protected GraphNode buildTrackFromGraph(BasicTrackControl tc, GraphNode node) {
return null;
}
/**
* Build a flow graph based on the given input format.
*/
GraphNode buildGraph(Format input) {
Log.comment("Input: " + input);
Vector candidates = new Vector();
GraphNode node = new GraphNode(null, (PlugIn)null,
input, null, 0);
indent = 1;
Log.setIndent(indent);
// Define the final targets.
if (!setDefaultTargets(input))
return null;
candidates.addElement(node);
GraphNode failed;
while ((node = buildGraph(candidates)) != null) {
// Found a potential graph. Verify it if all the
// nodes can be used.
if ((failed = verifyGraph(node)) == null) {
// we are done.
indent = 0;
Log.setIndent(indent);
return node;
}
// If we can't build a track from it, it's because there's
// a node in the graph that cannot be opened. We'll have
// to reap it from the candidates and the registry.
removeFailure(candidates, failed, input);
}
indent = 0;
Log.setIndent(indent);
return node;
}
/**
* Given the intermediate search candidates, build a graph until
* it reaches a target.
*/
GraphNode buildGraph(Vector candidates) {
GraphNode node;
while ((node = doBuildGraph(candidates)) == null) {
if (candidates.isEmpty())
break;
}
return node;
}
/**
* This is the "worker" method that does all the dirty work.
*/
GraphNode doBuildGraph(Vector candidates) {
if (candidates.isEmpty())
return null;
GraphNode node = (GraphNode)candidates.firstElement();
candidates.removeElementAt(0);
if (node.input == null &&
(node.plugin == null || !(node.plugin instanceof Codec))) {
// shouldn't happen!
Log.error("Internal error: doBuildGraph");
return null;
}
int oldIndent = indent;
Log.setIndent(node.level + 1);
//Log.write("level: " + node.level);
if (node.plugin != null) {
// It may not seem necessary to do this since the
// previous round has already verified the input.
// But since the same plugin could have a different
// input called on it on previous rounds, it needs to
// be resetted to the designated input. This has
// caused a bug in failing setOutputFormat for some
// codecs.
if (verifyInput(node.plugin, node.input) == null)
return null;
}
/*
Log.write("Try plugin: " + node.plugin.getClass());
else
Log.write("Given input: " + node.input);
*/
// Stop when the target is reached as defined by the findTarget
// method.
GraphNode n;
if ((n = findTarget(node)) != null) {
// We are done!
/*
if (n.plugin != null)
Log.write("Found target: " + n.plugin);
else
Log.write("Found target: " + n.cname);
*/
indent = oldIndent;
Log.setIndent(indent);
return n;
}
// Don't go deeper than allowed.
if (node.level >= STAGES) {
indent = oldIndent;
Log.setIndent(indent);
return null;
}
Format input, outs[];
boolean mp3Pkt = false; // 2.1.1b hack -ivg
if (node.plugin != null) {
if (node.output != null) {
outs = new Format[1];
outs[0] = node.output;
} else {
outs = node.getSupportedOutputs(node.input);
if (outs == null || outs.length == 0) {
//Log.write("Weird! The given plugin does not support any output.");
indent = oldIndent;
Log.setIndent(indent);
return null;
}
}
input = node.input;
// 2.1.1b hack -ivg
if (node.plugin instanceof
com.sun.media.codec.audio.mpa.Packetizer)
mp3Pkt = true;
} else {
outs = new Format[1];
outs[0] = node.input;
input = null;
}
GraphNode gn;
Format fmt, ins[];
boolean foundSomething = false;
for (int i = 0; i < outs.length; i++) {
// Ignore outputs that are the same as the input.
if (!node.custom && input != null && input.equals(outs[i]))
continue;
// Verify the output format.
if (node.plugin != null) {
if (verifyOutput(node.plugin, outs[i]) == null) {
//Log.write("Verify output failed: " + node.plugin);
//Log.write(" with: " + outs[i]);
if (inspector != null && inspector.detailMode())
inspector.verifyOutputFailed(node.plugin, outs[i]);
continue;
}
if (inspector != null &&
!inspector.verify((Codec)node.plugin, node.input, outs[i]))
continue;
}
//Log.write("find codec for input: " + outs[i]);
Vector cnames = PlugInManager.getPlugInList(outs[i],
null, PlugInManager.CODEC);
if (cnames == null || cnames.size() == 0)
continue;
for (int j = 0; j < cnames.size(); j++) {
// Instantiate and verify the codec.
if ((gn = getPlugInNode((String)cnames.elementAt(j),
PlugInManager.CODEC, plugIns)) == null)
continue;
// 2.1.1b hack -ivg
if (mp3Pkt && gn.plugin instanceof
com.sun.media.codec.audio.mpa.DePacketizer)
continue;
// Check to see if the particular input/plugin combination
// has already been attempted. If so, we don't need to
// do it again.
if (gn.checkAttempted(outs[i]))
continue;
//Log.write("Try codec: " + cnames.elementAt(j));
ins = gn.getSupportedInputs();
if ((fmt = matches(outs[i], ins, null, gn.plugin)) == null) {
//Log.write("Verify input failed: " + outs[i]);
//Log.write(" : " + gn.plugin);
if (inspector != null && inspector.detailMode())
inspector.verifyInputFailed(gn.plugin, outs[i]);
continue;
}
if (inspector != null && inspector.detailMode()) {
if (!inspector.verify((Codec)gn.plugin, fmt, null))
continue;
}
n = new GraphNode(gn, fmt, node, node.level+1);
candidates.addElement(n);
foundSomething = true;
}
}
/*
if (!foundSomething) {
if (node.plugin == null)
Log.write(" no codec supports the given input.");
else
Log.write(" no codec supports the outputs from this plugin.");
}
*/
indent = oldIndent;
Log.setIndent(indent);
return null;
}
/**
* This defines when the search ends. The "targets" array defines
* the nodes that are to be the "end points" (leaf nodes) of the
* graph.
* With the default graph builder, the targets array contains the
* list of sinks that can potentially support the input format.
*/
GraphNode findTarget(GraphNode node) {
Format outs[];
// Expand the outputs of the next node.
if (node.plugin == null) {
outs = new Format[1];
outs[0] = node.input;
} else {
if (node.output != null) {
outs = new Format[1];
outs[0] = node.output;
} else {
outs = node.getSupportedOutputs(node.input);
if (outs == null || outs.length == 0) {
//Log.write("Weird! The given plugin does not support any output.");
return null;
}
}
}
GraphNode n;
// Check for the list of predefined targets.
if (targetPlugins != null && (n = verifyTargetPlugins(node, outs)) != null)
return n;
return null;
}
/**
* Check for a match in the list of predefined targets.
*/
GraphNode verifyTargetPlugins(GraphNode node, Format outs[]) {
GraphNode gn;
Format fmt;
for (int i = 0; i < targetPlugins.length; i++) {
if ((gn = targetPlugins[i]) == null) {
String name = (String)targetPluginNames.elementAt(i);
if (name == null)
continue;
// Initial screening before instantiating the objects.
Format base[] = PlugInManager.getSupportedInputFormats(
name, targetType);
if (matches(outs, base, null, null) == null)
continue;
// Passing initial test, we'll want to instantiate it
// to get more info from it.
if ((gn = getPlugInNode(name, targetType,
plugIns)) == null) {
targetPluginNames.setElementAt(null, i);
continue;
}
targetPlugins[i] = gn;
}
if ((fmt = matches(outs, gn.getSupportedInputs(),
node.plugin, gn.plugin)) != null) {
// found the target.
if (inspector != null) {
if (node.plugin != null &&
!inspector.verify((Codec)node.plugin, node.input, fmt))
continue;
if ((gn.type == -1 ||
gn.type == PlugInManager.CODEC) &&
gn.plugin instanceof Codec) {
if (!inspector.verify((Codec)gn.plugin, fmt, null))
continue;
} else if ((gn.type == -1 ||
gn.type == PlugInManager.RENDERER) &&
gn.plugin instanceof Renderer) {
if (!inspector.verify((Renderer)gn.plugin, fmt))
continue;
}
}
return new GraphNode(gn, fmt, node, node.level+1);
}
}
return null;
}
/**
* Set the default targets, which are the renderers.
*/
boolean setDefaultTargets(Format in) {
return setDefaultTargetRenderer(in);
}
boolean setDefaultTargetRenderer(Format in) {
// Define the final targets which uses renderers.
if (in instanceof AudioFormat) {
targetPluginNames = PlugInManager.getPlugInList(
new AudioFormat(null,
AudioFormat.NOT_SPECIFIED,
AudioFormat.NOT_SPECIFIED,
AudioFormat.NOT_SPECIFIED,
AudioFormat.NOT_SPECIFIED,
AudioFormat.NOT_SPECIFIED,
AudioFormat.NOT_SPECIFIED,
AudioFormat.NOT_SPECIFIED,
null),
null, PlugInManager.RENDERER);
} else if (in instanceof VideoFormat) {
targetPluginNames = PlugInManager.getPlugInList(
new VideoFormat(null,
null,
VideoFormat.NOT_SPECIFIED,
null,
VideoFormat.NOT_SPECIFIED // frameRate ???
),
null, PlugInManager.RENDERER);
} else {
targetPluginNames = PlugInManager.getPlugInList(null,
null, PlugInManager.RENDERER);
}
// No target available.
if (targetPluginNames == null || targetPluginNames.size() == 0) {
//Log.write("The graph builder does not recognize the input format at all:");
//Log.write(in.toString());
return false;
}
targetPlugins = new GraphNode[targetPluginNames.size()];
targetType = PlugInManager.RENDERER;
return true;
}
/**
* Given a protential graph, verify it.
*/
protected GraphNode verifyGraph(GraphNode node) {
Format prevFormat = null;
Vector used = new Vector(5);
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 (used.contains(node.plugin)) {
// That plugin has already been used in the same path,
// we'll need to instantiate another one of its kind.
PlugIn p;
if (node.cname == null ||
(p = createPlugIn(node.cname, -1)) == null) {
Log.write("Failed to instantiate " + node.cname);
return node;
}
node.plugin = p;
} else {
used.addElement(node.plugin);
}
if ((node.type == -1 || node.type == PlugInManager.RENDERER) &&
node.plugin instanceof Renderer) {
((Renderer)node.plugin).setInputFormat(node.input);
} else if ((node.type == -1 || node.type == PlugInManager.CODEC) &&
node.plugin instanceof Codec) {
((Codec)node.plugin).setInputFormat(node.input);
if (prevFormat != null)
((Codec)node.plugin).setOutputFormat(prevFormat);
else if (node.output != null)
((Codec)node.plugin).setOutputFormat(node.output);
}
// For renderers, we wait till prefetching to
// open the device.
if (!((node.type == -1 || node.type == PlugInManager.RENDERER) &&
node.plugin instanceof Renderer)) {
try {
node.plugin.open();
} catch (Exception e) {
Log.warning("Failed to open: " + node.plugin);
node.failed = true;
return node;
}
}
prevFormat = node.input;
node = node.prev;
}
Log.setIndent(indent--);
return null;
}
/**
* Given a node that has failed, this function will eliminate
* it from the candiates list and mark it in the registry as
* as failed node so it won't be attempted again.
* This method is upated by hsy on 10/10/2000. If we just
* remove the failed node from candidates and mark it in the
* registy, we can't guarantee we could roll back to the exact/desired
* node/state to re-start searching, since this node might have already
* been removed from candidates. The simple/straightforward fix is
* to clear up candidates and plugIns and start allover again.
*/
void removeFailure(Vector candidates, GraphNode failed, Format input) {
if (failed.plugin == null)
return;
// This is the new implementation updated by hsy on 10/10/2000
// Here we re-start building the graph allover again. Clear
// candidates and put the initial node into it.
Log.comment("Failed to open plugin " + failed.plugin + ". Will re-build the graph allover again");
candidates.removeAllElements();
GraphNode hsyn = new GraphNode(null, (PlugIn)null,
input, null, 0);
indent = 1;
Log.setIndent(indent);
candidates.addElement(hsyn);
// clear up the hashtable plugIns too, only let it keep
// all the failed nodes, so that we are not going to
// attemp these nodes again.
failed.failed = true;
plugIns.put(failed.plugin.getClass().getName(), failed);
Enumeration e = plugIns.keys();
while(e.hasMoreElements()) {
String ss = (String)e.nextElement();
GraphNode nn = (GraphNode)plugIns.get(ss);
if ( !nn.failed)
plugIns.remove(ss);
}
/**** This is the old implementation.
GraphNode n;
Enumeration enum = candidates.elements();
while (enum.hasMoreElements()) {
n = (GraphNode)enum.nextElement();
if (n.plugin == failed.plugin)
candidates.removeElement(n);
}
if ((n = (GraphNode)plugIns.get(failed.plugin.getClass().getName())) != null)
n.failed = true;
******/
}
/**
* Given a codec class name, instantiate the codec and query it
* dynamically to see if it supports the given input and output formats.
*/
static public GraphNode getPlugInNode(String name, int type, Hashtable plugIns) {
GraphNode gn = null;
Object obj = null;
boolean add = false;
// Check the hash registry to see if we've already instantiated that
// object. If not, we'll instantiate it.
if (plugIns == null || (gn = (GraphNode)plugIns.get(name)) == null) {
PlugIn p = createPlugIn(name, type);
gn = new GraphNode(name, p, null, null, 0);
if (plugIns != null)
plugIns.put(name, gn);
if (p == null) {
// If we failed to create it this time, we won't try it again.
// We'll mark it as failed.
gn.failed = true;
return null;
} else
return gn;
}
// If it has been marked as failed before, we won't attempt
// to use it again.
if (gn.failed)
return null;
if (verifyClass(gn.plugin, type))
return gn;
return null;
}
/**
* Find a codec that can handle the given input and output.
* The output argument can be null if no specific output format
* is required.
*/
static public Codec findCodec(Format in, Format out,
Format selectedIn[], Format selectedOut[]) {
Vector cnames = PlugInManager.getPlugInList(in, out,
PlugInManager.CODEC);
if (cnames == null) {
// Well no codec supports that input. :(
return null;
}
Codec c = null;
Format fmts[], matched;
for (int i = 0; i < cnames.size(); i++) {
if ((c = (Codec)createPlugIn((String)cnames.elementAt(i),
PlugInManager.CODEC)) == null)
continue;
fmts = c.getSupportedInputFormats();
if ((matched = matches(in, fmts, null, c)) == null)
continue;
if (selectedIn != null && selectedIn.length > 0)
selectedIn[0] = matched;
fmts = c.getSupportedOutputFormats(matched);
if (fmts == null || fmts.length == 0) {
// Weird!
continue;
}
boolean success = false;
for (int j = 0; j < fmts.length; j++) {
// Try out the supported output formats in turn.
if (out != null) {
if (!out.matches(fmts[j]) ||
(matched = out.intersects(fmts[j])) == null)
continue;
} else
matched = fmts[j];
if (c.setOutputFormat(matched) != null) {
success = true;
break;
}
}
if (success) {
try {
c.open();
} catch (ResourceUnavailableException e) {
}
if (selectedOut != null && selectedOut.length > 0)
selectedOut[0] = matched;
// Alright, we are done!
return c;
}
}
return null;
}
/**
* Find a renderer that can handle the given input and output.
* The output argument can be null if no specific output format
* is required.
*/
static public Renderer findRenderer(Format in) {
Vector names = PlugInManager.getPlugInList(in, null,
PlugInManager.RENDERER);
if (names == null) {
// Well no renderer supports that input. :(
return null;
}
Renderer r = null;
Format fmts[], matched;
for (int i = 0; i < names.size(); i++) {
if ((r = (Renderer)createPlugIn((String)names.elementAt(i),
PlugInManager.RENDERER)) == null)
continue;
fmts = r.getSupportedInputFormats();
if ((matched = matches(in, fmts, null, r)) == null)
continue;
try {
r.open();
} catch (ResourceUnavailableException e) {
}
// Alright, we are done!
return r;
}
return null;
}
/**
* Return a chain of codecs and renderer to render to input format.
* Unlike findCodec and findRenderer, it uses the same graph building
* algorithm that the media engine uses to determine the best rendering
* path for a particular input format.
* The return value is a vector of plugins of all the codecs and
* the renderer.
* The plugin list is in reverse order starting from the renderer.
* The list of the corresponding input formats for each codec is
* also returned as an argument to the function.
*/
static public Vector findRenderingChain(Format in, Vector formats) {
SimpleGraphBuilder gb = new SimpleGraphBuilder();
GraphNode n;
if ((n = gb.buildGraph(in)) == null)
return null;
Vector list = new Vector(10);
while (n != null && n.plugin != null) {
list.addElement(n.plugin);
if (formats != null)
formats.addElement(n.input);
n = n.prev;
}
return list;
}
static public PlugIn createPlugIn(String name, int type) {
Class cls;
Object obj;
try {
// cls = Class.forName(name);
cls = BasicPlugIn.getClassForName(name);
obj = cls.newInstance();
} catch (Exception e) {
//Log.write("Cannot instantiate: " + name);
return null;
} catch (Error e) {
return null;
}
if (verifyClass(obj, type))
return (PlugIn)obj;
//Log.write(name + " is not of type " + cls);
return null;
}
static public boolean verifyClass(Object obj, int type) {
Class cls;
switch (type) {
case PlugInManager.CODEC:
cls = Codec.class; break;
case PlugInManager.RENDERER:
cls = Renderer.class; break;
case PlugInManager.MULTIPLEXER:
cls = Multiplexer.class; break;
default:
cls = PlugIn.class;
}
if (cls.isInstance(obj))
return true;
else
return false;
}
/**
* Choose a format among the two input arrays that matches and verify
* that if the given upstream and downstream plugins accept the matched
* format as output (for the upstream) or as input (for the downstream).
* Either of the plugin arguments can be null. In which case the
* verification step will be skipped accordingly.
* @param outs the supported output formats from the upstream node.
* @param ints the supported input formats from the downstream node.
* @param up the upstream node.
* @param down the downstream node.
* @return a matching format.
*/
static public Format matches(Format outs[], Format ins[], PlugIn up, PlugIn down) {
Format fmt;
if (outs == null) return null;
for (int i = 0; i < outs.length; i++) {
if ((fmt = matches(outs[i], ins, up, down)) != null)
return fmt;
}
return null;
}
/**
* Choose a format among the two input arrays that matches and verify
* that if the given upstream and downstream plugins accept the matched
* format as output (for the upstream) or as input (for the downstream).
* Either of the plugin arguments can be null. In which case the
* verification step will be skipped accordingly.
* @return a matching format.
*/
static public Format matches(Format out, Format ins[], PlugIn up, PlugIn down) {
if (out == null || ins == null) return null;
for (int i = 0; i < ins.length; i++) {
if (ins[i] != null &&
ins[i].getClass().isAssignableFrom(out.getClass()) &&
out.matches(ins[i]) ) {
Format fmt = out.intersects(ins[i]);
if (fmt == null)
// weird!
continue;
// Check if the downstream accepts the given input.
if (down != null && (fmt = verifyInput(down, fmt)) == null)
continue;
// Check if the upstream accepts the given as output.
Format refined = fmt;
if (up != null && (refined = verifyOutput(up, fmt)) == null)
continue;
// If the returned output format from the upstream is
// different from the original input to the upstream,
// we'll have to check that new format on the downstream
// to make sure.
if (down != null && refined != fmt &&
verifyInput(down, refined) == null)
continue;
return refined;
}
}
return null;
}
static public Format matches(Format outs[], Format in, PlugIn up, PlugIn down) {
Format ins[] = new Format[1];
ins[0] = in;
return matches(outs, ins, up, down);
}
/**
* Check if the given plugin supports the given input.
*/
static public Format verifyInput(PlugIn p, Format in) {
if (p instanceof Codec)
return ((Codec)p).setInputFormat(in);
if (p instanceof Renderer)
return ((Renderer)p).setInputFormat(in);
return null;
}
/**
* Check if the given plugin supports the given output.
*/
static public Format verifyOutput(PlugIn p, Format out) {
if (p instanceof Codec)
return ((Codec)p).setOutputFormat(out);
return null;
}
}
|