/* * 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 mFilterMap = new HashMap(); /** 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 mSubGraphs = new HashSet(); /** 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 mFilterMap = new HashMap(); /** * 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 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 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 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!"); } } }