/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media.filterfw;
import android.util.Log;
import android.view.View;
import androidx.media.filterpacks.base.BranchFilter;
import androidx.media.filterpacks.base.FrameSlotSource;
import androidx.media.filterpacks.base.FrameSlotTarget;
import androidx.media.filterpacks.base.GraphInputSource;
import androidx.media.filterpacks.base.GraphOutputTarget;
import androidx.media.filterpacks.base.ValueTarget;
import androidx.media.filterpacks.base.ValueTarget.ValueListener;
import androidx.media.filterpacks.base.VariableSource;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
/**
* A graph of Filter nodes.
*
* A FilterGraph instance contains a set of Filter instances connected by their output and input
* ports. Every filter belongs to exactly one graph and cannot be moved to another graph.
*
* FilterGraphs may contain sub-graphs that are dependent on the parent graph. These are typically
* used when inserting sub-graphs into MetaFilters. When a parent graph is torn down so are its
* sub-graphs. The same applies to flushing frames of a graph.
*/
public class FilterGraph {
private final static boolean DEBUG = false;
/** The context that this graph lives in */
private MffContext mContext;
/** Map from name of filter to the filter instance */
private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>();
/** Allows quick access to array of all filters. */
private Filter[] mAllFilters = null;
/** The GraphRunner currently attached to this graph */
GraphRunner mRunner;
/** The set of sub-graphs of this graph */
HashSet<FilterGraph> mSubGraphs = new HashSet<FilterGraph>();
/** The parent graph of this graph, or null it this graph is a root graph. */
private FilterGraph mParentGraph;
public static class Builder {
/** The context that this builder lives in */
private MffContext mContext;
/** Map from name of filter to the filter instance */
private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>();
/**
* Creates a new builder for specifying a graph structure.
* @param context The context the graph will live in.
*/
public Builder(MffContext context) {
mContext = context;
}
/**
* Add a filter to the graph.
*
* Adds the specified filter to the set of filters of this graph. The filter must not be in
* the graph already, and the filter's name must be unique within the graph.
*
* @param filter the filter to add to the graph.
* @throws IllegalArgumentException if the filter is in the graph already, or its name is
* is already taken.
*/
public void addFilter(Filter filter) {
if (mFilterMap.values().contains(filter)) {
throw new IllegalArgumentException("Attempting to add filter " + filter + " that "
+ "is in the graph already!");
} else if (mFilterMap.containsKey(filter.getName())) {
throw new IllegalArgumentException("Graph contains filter with name '"
+ filter.getName() + "' already!");
} else {
mFilterMap.put(filter.getName(), filter);
}
}
/**
* Adds a variable to the graph.
*
* TODO: More documentation.
*
* @param name the name of the variable.
* @param value the value of the variable or null if no value is to be set yet.
* @return the VariableSource filter that holds the value of this variable.
*/
public VariableSource addVariable(String name, Object value) {
if (getFilter(name) != null) {
throw new IllegalArgumentException("Filter named '" + name + "' exists already!");
}
VariableSource valueSource = new VariableSource(mContext, name);
addFilter(valueSource);
if (value != null) {
valueSource.setValue(value);
}
return valueSource;
}
public FrameSlotSource addFrameSlotSource(String name, String slotName) {
FrameSlotSource filter = new FrameSlotSource(mContext, name, slotName);
addFilter(filter);
return filter;
}
public FrameSlotTarget addFrameSlotTarget(String name, String slotName) {
FrameSlotTarget filter = new FrameSlotTarget(mContext, name, slotName);
addFilter(filter);
return filter;
}
/**
* Connect two filters by their ports.
* The filters specified must have been previously added to the graph builder.
*
* @param sourceFilterName The name of the source filter.
* @param sourcePort The name of the source port.
* @param targetFilterName The name of the target filter.
* @param targetPort The name of the target port.
*/
public void connect(String sourceFilterName, String sourcePort,
String targetFilterName, String targetPort) {
Filter sourceFilter = getFilter(sourceFilterName);
Filter targetFilter = getFilter(targetFilterName);
if (sourceFilter == null) {
throw new IllegalArgumentException("Unknown filter '" + sourceFilterName + "'!");
} else if (targetFilter == null) {
throw new IllegalArgumentException("Unknown filter '" + targetFilterName + "'!");
}
connect(sourceFilter, sourcePort, targetFilter, targetPort);
}
/**
* Connect two filters by their ports.
* The filters specified must have been previously added to the graph builder.
*
* @param sourceFilter The source filter.
* @param sourcePort The name of the source port.
* @param targetFilter The target filter.
* @param targetPort The name of the target port.
*/
public void connect(Filter sourceFilter, String sourcePort,
Filter targetFilter, String targetPort) {
sourceFilter.connect(sourcePort, targetFilter, targetPort);
}
/**
* Returns the filter with the specified name.
*
* @return the filter with the specified name, or null if no such filter exists.
*/
public Filter getFilter(String name) {
return mFilterMap.get(name);
}
/**
* Builds the graph and checks signatures.
*
* @return The new graph instance.
*/
public FilterGraph build() {
checkSignatures();
return buildWithParent(null);
}
/**
* Builds the sub-graph and checks signatures.
*
* @param parentGraph the parent graph of the built sub-graph.
* @return The new graph instance.
*/
public FilterGraph buildSubGraph(FilterGraph parentGraph) {
if (parentGraph == null) {
throw new NullPointerException("Parent graph must be non-null!");
}
checkSignatures();
return buildWithParent(parentGraph);
}
VariableSource assignValueToFilterInput(Object value, String filterName, String inputName) {
// Get filter to connect to
Filter filter = getFilter(filterName);
if (filter == null) {
throw new IllegalArgumentException("Unknown filter '" + filterName + "'!");
}
// Construct a name for our value source and make sure it does not exist already
String valueSourceName = filterName + "." + inputName;
if (getFilter(valueSourceName) != null) {
throw new IllegalArgumentException("VariableSource for '" + filterName + "' and "
+ "input '" + inputName + "' exists already!");
}
// Create new VariableSource and connect it to the target filter and port
VariableSource valueSource = new VariableSource(mContext, valueSourceName);
addFilter(valueSource);
try {
((Filter)valueSource).connect("value", filter, inputName);
} catch (RuntimeException e) {
throw new RuntimeException("Could not connect VariableSource to input '" + inputName
+ "' of filter '" + filterName + "'!", e);
}
// Assign the value to the VariableSource
if (value != null) {
valueSource.setValue(value);
}
return valueSource;
}
VariableSource assignVariableToFilterInput(String varName,
String filterName,
String inputName) {
// Get filter to connect to
Filter filter = getFilter(filterName);
if (filter == null) {
throw new IllegalArgumentException("Unknown filter '" + filterName + "'!");
}
// Get variable
Filter variable = getFilter(varName);
if (variable == null || !(variable instanceof VariableSource)) {
throw new IllegalArgumentException("Unknown variable '" + varName + "'!");
}
// Connect variable (and possibly branch) variable to filter
try {
connectAndBranch(variable, "value", filter, inputName);
} catch (RuntimeException e) {
throw new RuntimeException("Could not connect VariableSource to input '" + inputName
+ "' of filter '" + filterName + "'!", e);
}
return (VariableSource)variable;
}
/**
* Builds the graph without checking signatures.
* If parent is non-null, build a sub-graph of the specified parent.
*
* @return The new graph instance.
*/
private FilterGraph buildWithParent(FilterGraph parent) {
FilterGraph graph = new FilterGraph(mContext, parent);
graph.mFilterMap = mFilterMap;
graph.mAllFilters = mFilterMap.values().toArray(new Filter[0]);
for (Entry<String, Filter> filterEntry : mFilterMap.entrySet()) {
filterEntry.getValue().insertIntoFilterGraph(graph);
}
return graph;
}
private void checkSignatures() {
checkSignaturesForFilters(mFilterMap.values());
}
// TODO: Currently this always branches even if the connection is a 1:1 connection. Later
// we may optimize to pass through directly in the 1:1 case (may require disconnecting
// ports).
private void connectAndBranch(Filter sourceFilter,
String sourcePort,
Filter targetFilter,
String targetPort) {
String branchName = "__" + sourceFilter.getName() + "_" + sourcePort + "Branch";
Filter branch = getFilter(branchName);
if (branch == null) {
branch = new BranchFilter(mContext, branchName, false);
addFilter(branch);
sourceFilter.connect(sourcePort, branch, "input");
}
String portName = "to" + targetFilter.getName() + "_" + targetPort;
branch.connect(portName, targetFilter, targetPort);
}
}
/**
* Attach the graph and its subgraphs to a custom GraphRunner.
*
* Call this if you want the graph to be executed by a specific GraphRunner. You must call
* this before any other runner is set. Note that calls to {@code getRunner()} and
* {@code run()} auto-create a GraphRunner.
*
* @param runner The GraphRunner instance that should execute this graph.
* @see #getRunner()
* @see #run()
*/
public void attachToRunner(GraphRunner runner) {
if (mRunner == null) {
for (FilterGraph subGraph : mSubGraphs) {
subGraph.attachToRunner(runner);
}
runner.attachGraph(this);
mRunner = runner;
} else if (mRunner != runner) {
throw new RuntimeException("Cannot attach FilterGraph to GraphRunner that is already "
+ "attached to another GraphRunner!");
}
}
/**
* Forcibly tear down a filter graph.
*
* Call this to release any resources associated with the filter graph, its filters and any of
* its sub-graphs. This method must not be called if the graph (or any sub-graph) is running.
*
* You may no longer access this graph instance or any of its subgraphs after calling this
* method.
*
* Tearing down of sub-graphs is not supported. You must tear down the root graph, which will
* tear down all of its sub-graphs.
*
* @throws IllegalStateException if the graph is still running.
* @throws RuntimeException if you attempt to tear down a sub-graph.
*/
public void tearDown() {
assertNotRunning();
if (mParentGraph != null) {
throw new RuntimeException("Attempting to tear down sub-graph!");
}
if (mRunner != null) {
mRunner.tearDownGraph(this);
}
for (FilterGraph subGraph : mSubGraphs) {
subGraph.mParentGraph = null;
subGraph.tearDown();
}
mSubGraphs.clear();
}
/**
* Returns the context of the graph.
*
* @return the MffContext instance that this graph is bound to.
*/
public MffContext getContext() {
return mContext;
}
/**
* Returns the filter with the specified name.
*
* @return the filter with the specified name, or null if no such filter exists.
*/
public Filter getFilter(String name) {
return mFilterMap.get(name);
}
/**
* Returns the VariableSource for the specified variable.
*
* TODO: More documentation.
* TODO: More specialized error handling.
*
* @param name The name of the VariableSource.
* @return The VariableSource filter instance with the specified name.
*/
public VariableSource getVariable(String name) {
Filter result = mFilterMap.get(name);
if (result != null && result instanceof VariableSource) {
return (VariableSource)result;
} else {
throw new IllegalArgumentException("Unknown variable '" + name + "' specified!");
}
}
/**
* Returns the GraphOutputTarget with the specified name.
*
* @param name The name of the target.
* @return The GraphOutputTarget instance with the specified name.
*/
public GraphOutputTarget getGraphOutput(String name) {
Filter result = mFilterMap.get(name);
if (result != null && result instanceof GraphOutputTarget) {
return (GraphOutputTarget)result;
} else {
throw new IllegalArgumentException("Unknown target '" + name + "' specified!");
}
}
/**
* Returns the GraphInputSource with the specified name.
*
* @param name The name of the source.
* @return The GraphInputSource instance with the specified name.
*/
public GraphInputSource getGraphInput(String name) {
Filter result = mFilterMap.get(name);
if (result != null && result instanceof GraphInputSource) {
return (GraphInputSource)result;
} else {
throw new IllegalArgumentException("Unknown source '" + name + "' specified!");
}
}
/**
* Binds a filter to a view.
*
* ViewFilter instances support visualizing their data to a view. See the specific filter
* documentation for details. Views may be bound only if the graph is not running.
*
* @param filterName the name of the filter to bind.
* @param view the view to bind to.
* @throws IllegalStateException if the filter is in an illegal state.
* @throws IllegalArgumentException if no such view-filter exists.
*/
public void bindFilterToView(String filterName, View view) {
Filter filter = mFilterMap.get(filterName);
if (filter != null && filter instanceof ViewFilter) {
((ViewFilter)filter).bindToView(view);
} else {
throw new IllegalArgumentException("Unknown view filter '" + filterName + "'!");
}
}
/**
* TODO: Documentation.
*/
public void bindValueTarget(String filterName, ValueListener listener, boolean onCallerThread) {
Filter filter = mFilterMap.get(filterName);
if (filter != null && filter instanceof ValueTarget) {
((ValueTarget)filter).setListener(listener, onCallerThread);
} else {
throw new IllegalArgumentException("Unknown ValueTarget filter '" + filterName + "'!");
}
}
// Running Graphs //////////////////////////////////////////////////////////////////////////////
/**
* Convenience method to run the graph.
*
* Creates a new runner for this graph in the specified mode and executes it. Returns the
* runner to allow control of execution.
*
* @throws IllegalStateException if the graph is already running.
* @return the GraphRunner instance that was used for execution.
*/
public GraphRunner run() {
GraphRunner runner = getRunner();
runner.setIsVerbose(false);
runner.start(this);
return runner;
}
/**
* Returns the GraphRunner for this graph.
*
* Every FilterGraph instance has a GraphRunner instance associated with it for executing the
* graph.
*
* @return the GraphRunner instance for this graph.
*/
public GraphRunner getRunner() {
if (mRunner == null) {
GraphRunner runner = new GraphRunner(mContext);
attachToRunner(runner);
}
return mRunner;
}
/**
* Returns whether the graph is currently running.
*
* @return true if the graph is currently running.
*/
public boolean isRunning() {
return mRunner != null && mRunner.isRunning();
}
/**
* Check each filter's signatures if all requirements are fulfilled.
*
* This will throw a RuntimeException if any unfulfilled requirements are found.
* Note that FilterGraph.Builder also has a function checkSignatures(), which allows
* to do the same /before/ the FilterGraph is built.
*/
public void checkSignatures() {
checkSignaturesForFilters(mFilterMap.values());
}
// MFF Internal Methods ////////////////////////////////////////////////////////////////////////
Filter[] getAllFilters() {
return mAllFilters;
}
static void checkSignaturesForFilters(Collection<Filter> filters) {
for (Filter filter : filters) {
if (DEBUG) {
Log.d("FilterGraph", "Checking filter " + filter.getName() + "...");
}
Signature signature = filter.getSignature();
signature.checkInputPortsConform(filter);
signature.checkOutputPortsConform(filter);
}
}
/**
* Wipes the filter references in this graph, so that they may be collected.
*
* This must be called only after a tearDown as this will make the FilterGraph invalid.
*/
void wipe() {
mAllFilters = null;
mFilterMap = null;
}
void flushFrames() {
for (Filter filter : mFilterMap.values()) {
for (InputPort inputPort : filter.getConnectedInputPorts()) {
inputPort.clear();
}
for (OutputPort outputPort : filter.getConnectedOutputPorts()) {
outputPort.clear();
}
}
}
Set<FilterGraph> getSubGraphs() {
return mSubGraphs;
}
// Internal Methods ////////////////////////////////////////////////////////////////////////////
private FilterGraph(MffContext context, FilterGraph parentGraph) {
mContext = context;
mContext.addGraph(this);
if (parentGraph != null) {
mParentGraph = parentGraph;
mParentGraph.mSubGraphs.add(this);
}
}
private void assertNotRunning() {
if (isRunning()) {
throw new IllegalStateException("Attempting to modify running graph!");
}
}
}