1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.media.filterfw;
18 
19 import android.util.Log;
20 import android.view.View;
21 import androidx.media.filterpacks.base.BranchFilter;
22 import androidx.media.filterpacks.base.FrameSlotSource;
23 import androidx.media.filterpacks.base.FrameSlotTarget;
24 import androidx.media.filterpacks.base.GraphInputSource;
25 import androidx.media.filterpacks.base.GraphOutputTarget;
26 import androidx.media.filterpacks.base.ValueTarget;
27 import androidx.media.filterpacks.base.ValueTarget.ValueListener;
28 import androidx.media.filterpacks.base.VariableSource;
29 
30 import java.util.Collection;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Map.Entry;
34 import java.util.Set;
35 
36 /**
37  * A graph of Filter nodes.
38  *
39  * A FilterGraph instance contains a set of Filter instances connected by their output and input
40  * ports. Every filter belongs to exactly one graph and cannot be moved to another graph.
41  *
42  * FilterGraphs may contain sub-graphs that are dependent on the parent graph. These are typically
43  * used when inserting sub-graphs into MetaFilters. When a parent graph is torn down so are its
44  * sub-graphs. The same applies to flushing frames of a graph.
45  */
46 public class FilterGraph {
47 
48     private final static boolean DEBUG = false;
49 
50     /** The context that this graph lives in */
51     private MffContext mContext;
52 
53     /** Map from name of filter to the filter instance */
54     private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>();
55 
56     /** Allows quick access to array of all filters. */
57     private Filter[] mAllFilters = null;
58 
59     /** The GraphRunner currently attached to this graph */
60     GraphRunner mRunner;
61 
62     /** The set of sub-graphs of this graph */
63     HashSet<FilterGraph> mSubGraphs = new HashSet<FilterGraph>();
64 
65     /** The parent graph of this graph, or null it this graph is a root graph. */
66     private FilterGraph mParentGraph;
67 
68     public static class Builder {
69 
70         /** The context that this builder lives in */
71         private MffContext mContext;
72 
73         /** Map from name of filter to the filter instance */
74         private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>();
75 
76         /**
77          * Creates a new builder for specifying a graph structure.
78          * @param context The context the graph will live in.
79          */
Builder(MffContext context)80         public Builder(MffContext context) {
81             mContext = context;
82         }
83 
84         /**
85          * Add a filter to the graph.
86          *
87          * Adds the specified filter to the set of filters of this graph. The filter must not be in
88          * the graph already, and the filter's name must be unique within the graph.
89          *
90          * @param filter the filter to add to the graph.
91          * @throws IllegalArgumentException if the filter is in the graph already, or its name is
92          *                                  is already taken.
93          */
addFilter(Filter filter)94         public void addFilter(Filter filter) {
95             if (mFilterMap.values().contains(filter)) {
96                 throw new IllegalArgumentException("Attempting to add filter " + filter + " that "
97                     + "is in the graph already!");
98             } else if (mFilterMap.containsKey(filter.getName())) {
99                 throw new IllegalArgumentException("Graph contains filter with name '"
100                     + filter.getName() + "' already!");
101             } else {
102                 mFilterMap.put(filter.getName(), filter);
103             }
104         }
105 
106         /**
107          * Adds a variable to the graph.
108          *
109          * TODO: More documentation.
110          *
111          * @param name the name of the variable.
112          * @param value the value of the variable or null if no value is to be set yet.
113          * @return the VariableSource filter that holds the value of this variable.
114          */
addVariable(String name, Object value)115         public VariableSource addVariable(String name, Object value) {
116             if (getFilter(name) != null) {
117                 throw new IllegalArgumentException("Filter named '" + name + "' exists already!");
118             }
119             VariableSource valueSource = new VariableSource(mContext, name);
120             addFilter(valueSource);
121             if (value != null) {
122                 valueSource.setValue(value);
123             }
124             return valueSource;
125         }
126 
addFrameSlotSource(String name, String slotName)127         public FrameSlotSource addFrameSlotSource(String name, String slotName) {
128             FrameSlotSource filter = new FrameSlotSource(mContext, name, slotName);
129             addFilter(filter);
130             return filter;
131         }
132 
addFrameSlotTarget(String name, String slotName)133         public FrameSlotTarget addFrameSlotTarget(String name, String slotName) {
134             FrameSlotTarget filter = new FrameSlotTarget(mContext, name, slotName);
135             addFilter(filter);
136             return filter;
137         }
138 
139         /**
140          * Connect two filters by their ports.
141          * The filters specified must have been previously added to the graph builder.
142          *
143          * @param sourceFilterName The name of the source filter.
144          * @param sourcePort The name of the source port.
145          * @param targetFilterName The name of the target filter.
146          * @param targetPort The name of the target port.
147          */
connect(String sourceFilterName, String sourcePort, String targetFilterName, String targetPort)148         public void connect(String sourceFilterName, String sourcePort,
149                             String targetFilterName, String targetPort) {
150             Filter sourceFilter = getFilter(sourceFilterName);
151             Filter targetFilter = getFilter(targetFilterName);
152             if (sourceFilter == null) {
153                 throw new IllegalArgumentException("Unknown filter '" + sourceFilterName + "'!");
154             } else if (targetFilter == null) {
155                 throw new IllegalArgumentException("Unknown filter '" + targetFilterName + "'!");
156             }
157             connect(sourceFilter, sourcePort, targetFilter, targetPort);
158         }
159 
160         /**
161          * Connect two filters by their ports.
162          * The filters specified must have been previously added to the graph builder.
163          *
164          * @param sourceFilter The source filter.
165          * @param sourcePort The name of the source port.
166          * @param targetFilter The target filter.
167          * @param targetPort The name of the target port.
168          */
connect(Filter sourceFilter, String sourcePort, Filter targetFilter, String targetPort)169         public void connect(Filter sourceFilter, String sourcePort,
170                             Filter targetFilter, String targetPort) {
171             sourceFilter.connect(sourcePort, targetFilter, targetPort);
172         }
173 
174         /**
175          * Returns the filter with the specified name.
176          *
177          * @return the filter with the specified name, or null if no such filter exists.
178          */
getFilter(String name)179         public Filter getFilter(String name) {
180             return mFilterMap.get(name);
181         }
182 
183         /**
184          * Builds the graph and checks signatures.
185          *
186          * @return The new graph instance.
187          */
build()188         public FilterGraph build() {
189             checkSignatures();
190             return buildWithParent(null);
191         }
192 
193         /**
194          * Builds the sub-graph and checks signatures.
195          *
196          * @param parentGraph the parent graph of the built sub-graph.
197          * @return The new graph instance.
198          */
buildSubGraph(FilterGraph parentGraph)199         public FilterGraph buildSubGraph(FilterGraph parentGraph) {
200             if (parentGraph == null) {
201                 throw new NullPointerException("Parent graph must be non-null!");
202             }
203             checkSignatures();
204             return buildWithParent(parentGraph);
205         }
206 
assignValueToFilterInput(Object value, String filterName, String inputName)207         VariableSource assignValueToFilterInput(Object value, String filterName, String inputName) {
208             // Get filter to connect to
209             Filter filter = getFilter(filterName);
210             if (filter == null) {
211                 throw new IllegalArgumentException("Unknown filter '" + filterName + "'!");
212             }
213 
214             // Construct a name for our value source and make sure it does not exist already
215             String valueSourceName = filterName + "." + inputName;
216             if (getFilter(valueSourceName) != null) {
217                 throw new IllegalArgumentException("VariableSource for '" + filterName + "' and "
218                     + "input '" + inputName + "' exists already!");
219             }
220 
221             // Create new VariableSource and connect it to the target filter and port
222             VariableSource valueSource = new VariableSource(mContext, valueSourceName);
223             addFilter(valueSource);
224             try {
225                 ((Filter)valueSource).connect("value", filter, inputName);
226             } catch (RuntimeException e) {
227                 throw new RuntimeException("Could not connect VariableSource to input '" + inputName
228                     + "' of filter '" + filterName + "'!", e);
229             }
230 
231             // Assign the value to the VariableSource
232             if (value != null) {
233                 valueSource.setValue(value);
234             }
235 
236             return valueSource;
237         }
238 
assignVariableToFilterInput(String varName, String filterName, String inputName)239         VariableSource assignVariableToFilterInput(String varName,
240                                                    String filterName,
241                                                    String inputName) {
242             // Get filter to connect to
243             Filter filter = getFilter(filterName);
244             if (filter == null) {
245                 throw new IllegalArgumentException("Unknown filter '" + filterName + "'!");
246             }
247 
248             // Get variable
249             Filter variable = getFilter(varName);
250             if (variable == null || !(variable instanceof VariableSource)) {
251                 throw new IllegalArgumentException("Unknown variable '" + varName + "'!");
252             }
253 
254             // Connect variable (and possibly branch) variable to filter
255             try {
256                 connectAndBranch(variable, "value", filter, inputName);
257             } catch (RuntimeException e) {
258                 throw new RuntimeException("Could not connect VariableSource to input '" + inputName
259                     + "' of filter '" + filterName + "'!", e);
260             }
261 
262             return (VariableSource)variable;
263         }
264 
265         /**
266          * Builds the graph without checking signatures.
267          * If parent is non-null, build a sub-graph of the specified parent.
268          *
269          * @return The new graph instance.
270          */
buildWithParent(FilterGraph parent)271         private FilterGraph buildWithParent(FilterGraph parent) {
272             FilterGraph graph = new FilterGraph(mContext, parent);
273             graph.mFilterMap = mFilterMap;
274             graph.mAllFilters = mFilterMap.values().toArray(new Filter[0]);
275             for (Entry<String, Filter> filterEntry : mFilterMap.entrySet()) {
276                 filterEntry.getValue().insertIntoFilterGraph(graph);
277             }
278             return graph;
279         }
280 
checkSignatures()281         private void checkSignatures() {
282             checkSignaturesForFilters(mFilterMap.values());
283         }
284 
285         // TODO: Currently this always branches even if the connection is a 1:1 connection. Later
286         // we may optimize to pass through directly in the 1:1 case (may require disconnecting
287         // ports).
connectAndBranch(Filter sourceFilter, String sourcePort, Filter targetFilter, String targetPort)288         private void connectAndBranch(Filter sourceFilter,
289                                       String sourcePort,
290                                       Filter targetFilter,
291                                       String targetPort) {
292             String branchName = "__" + sourceFilter.getName() + "_" + sourcePort + "Branch";
293             Filter branch = getFilter(branchName);
294             if (branch == null) {
295                 branch = new BranchFilter(mContext, branchName, false);
296                 addFilter(branch);
297                 sourceFilter.connect(sourcePort, branch, "input");
298             }
299             String portName = "to" + targetFilter.getName() + "_" + targetPort;
300             branch.connect(portName, targetFilter, targetPort);
301         }
302 
303     }
304 
305     /**
306      * Attach the graph and its subgraphs to a custom GraphRunner.
307      *
308      * Call this if you want the graph to be executed by a specific GraphRunner. You must call
309      * this before any other runner is set. Note that calls to {@code getRunner()} and
310      * {@code run()} auto-create a GraphRunner.
311      *
312      * @param runner The GraphRunner instance that should execute this graph.
313      * @see #getRunner()
314      * @see #run()
315      */
attachToRunner(GraphRunner runner)316     public void attachToRunner(GraphRunner runner) {
317         if (mRunner == null) {
318             for (FilterGraph subGraph : mSubGraphs) {
319                 subGraph.attachToRunner(runner);
320             }
321             runner.attachGraph(this);
322             mRunner = runner;
323         } else if (mRunner != runner) {
324             throw new RuntimeException("Cannot attach FilterGraph to GraphRunner that is already "
325                 + "attached to another GraphRunner!");
326         }
327     }
328 
329     /**
330      * Forcibly tear down a filter graph.
331      *
332      * Call this to release any resources associated with the filter graph, its filters and any of
333      * its sub-graphs. This method must not be called if the graph (or any sub-graph) is running.
334      *
335      * You may no longer access this graph instance or any of its subgraphs after calling this
336      * method.
337      *
338      * Tearing down of sub-graphs is not supported. You must tear down the root graph, which will
339      * tear down all of its sub-graphs.
340      *
341      * @throws IllegalStateException if the graph is still running.
342      * @throws RuntimeException if you attempt to tear down a sub-graph.
343      */
tearDown()344     public void tearDown() {
345         assertNotRunning();
346         if (mParentGraph != null) {
347             throw new RuntimeException("Attempting to tear down sub-graph!");
348         }
349         if (mRunner != null) {
350             mRunner.tearDownGraph(this);
351         }
352         for (FilterGraph subGraph : mSubGraphs) {
353             subGraph.mParentGraph = null;
354             subGraph.tearDown();
355         }
356         mSubGraphs.clear();
357     }
358 
359     /**
360      * Returns the context of the graph.
361      *
362      * @return the MffContext instance that this graph is bound to.
363      */
getContext()364     public MffContext getContext() {
365         return mContext;
366     }
367 
368     /**
369      * Returns the filter with the specified name.
370      *
371      * @return the filter with the specified name, or null if no such filter exists.
372      */
getFilter(String name)373     public Filter getFilter(String name) {
374         return mFilterMap.get(name);
375     }
376 
377     /**
378      * Returns the VariableSource for the specified variable.
379      *
380      * TODO: More documentation.
381      * TODO: More specialized error handling.
382      *
383      * @param name The name of the VariableSource.
384      * @return The VariableSource filter instance with the specified name.
385      */
getVariable(String name)386     public VariableSource getVariable(String name) {
387         Filter result = mFilterMap.get(name);
388         if (result != null && result instanceof VariableSource) {
389             return (VariableSource)result;
390         } else {
391             throw new IllegalArgumentException("Unknown variable '" + name + "' specified!");
392         }
393     }
394 
395     /**
396      * Returns the GraphOutputTarget with the specified name.
397      *
398      * @param name The name of the target.
399      * @return The GraphOutputTarget instance with the specified name.
400      */
getGraphOutput(String name)401     public GraphOutputTarget getGraphOutput(String name) {
402         Filter result = mFilterMap.get(name);
403         if (result != null && result instanceof GraphOutputTarget) {
404             return (GraphOutputTarget)result;
405         } else {
406             throw new IllegalArgumentException("Unknown target '" + name + "' specified!");
407         }
408     }
409 
410     /**
411      * Returns the GraphInputSource with the specified name.
412      *
413      * @param name The name of the source.
414      * @return The GraphInputSource instance with the specified name.
415      */
getGraphInput(String name)416     public GraphInputSource getGraphInput(String name) {
417         Filter result = mFilterMap.get(name);
418         if (result != null && result instanceof GraphInputSource) {
419             return (GraphInputSource)result;
420         } else {
421             throw new IllegalArgumentException("Unknown source '" + name + "' specified!");
422         }
423     }
424 
425     /**
426      * Binds a filter to a view.
427      *
428      * ViewFilter instances support visualizing their data to a view. See the specific filter
429      * documentation for details. Views may be bound only if the graph is not running.
430      *
431      * @param filterName the name of the filter to bind.
432      * @param view the view to bind to.
433      * @throws IllegalStateException if the filter is in an illegal state.
434      * @throws IllegalArgumentException if no such view-filter exists.
435      */
bindFilterToView(String filterName, View view)436     public void bindFilterToView(String filterName, View view) {
437         Filter filter = mFilterMap.get(filterName);
438         if (filter != null && filter instanceof ViewFilter) {
439             ((ViewFilter)filter).bindToView(view);
440         } else {
441             throw new IllegalArgumentException("Unknown view filter '" + filterName + "'!");
442         }
443     }
444 
445     /**
446      * TODO: Documentation.
447      */
bindValueTarget(String filterName, ValueListener listener, boolean onCallerThread)448     public void bindValueTarget(String filterName, ValueListener listener, boolean onCallerThread) {
449         Filter filter = mFilterMap.get(filterName);
450         if (filter != null && filter instanceof ValueTarget) {
451             ((ValueTarget)filter).setListener(listener, onCallerThread);
452         } else {
453             throw new IllegalArgumentException("Unknown ValueTarget filter '" + filterName + "'!");
454         }
455     }
456 
457     // Running Graphs //////////////////////////////////////////////////////////////////////////////
458     /**
459      * Convenience method to run the graph.
460      *
461      * Creates a new runner for this graph in the specified mode and executes it. Returns the
462      * runner to allow control of execution.
463      *
464      * @throws IllegalStateException if the graph is already running.
465      * @return the GraphRunner instance that was used for execution.
466      */
run()467     public GraphRunner run() {
468         GraphRunner runner = getRunner();
469         runner.setIsVerbose(false);
470         runner.start(this);
471         return runner;
472     }
473 
474     /**
475      * Returns the GraphRunner for this graph.
476      *
477      * Every FilterGraph instance has a GraphRunner instance associated with it for executing the
478      * graph.
479      *
480      * @return the GraphRunner instance for this graph.
481      */
getRunner()482     public GraphRunner getRunner() {
483         if (mRunner == null) {
484             GraphRunner runner = new GraphRunner(mContext);
485             attachToRunner(runner);
486         }
487         return mRunner;
488     }
489 
490     /**
491      * Returns whether the graph is currently running.
492      *
493      * @return true if the graph is currently running.
494      */
isRunning()495     public boolean isRunning() {
496         return mRunner != null && mRunner.isRunning();
497     }
498 
499     /**
500      * Check each filter's signatures if all requirements are fulfilled.
501      *
502      * This will throw a RuntimeException if any unfulfilled requirements are found.
503      * Note that FilterGraph.Builder also has a function checkSignatures(), which allows
504      * to do the same /before/ the FilterGraph is built.
505      */
checkSignatures()506     public void checkSignatures() {
507         checkSignaturesForFilters(mFilterMap.values());
508     }
509 
510     // MFF Internal Methods ////////////////////////////////////////////////////////////////////////
getAllFilters()511     Filter[] getAllFilters() {
512         return mAllFilters;
513     }
514 
checkSignaturesForFilters(Collection<Filter> filters)515     static void checkSignaturesForFilters(Collection<Filter> filters) {
516         for (Filter filter : filters) {
517             if (DEBUG) {
518                 Log.d("FilterGraph", "Checking filter " + filter.getName() + "...");
519             }
520             Signature signature = filter.getSignature();
521             signature.checkInputPortsConform(filter);
522             signature.checkOutputPortsConform(filter);
523         }
524     }
525 
526     /**
527      * Wipes the filter references in this graph, so that they may be collected.
528      *
529      * This must be called only after a tearDown as this will make the FilterGraph invalid.
530      */
wipe()531     void wipe() {
532         mAllFilters = null;
533         mFilterMap = null;
534     }
535 
flushFrames()536     void flushFrames() {
537         for (Filter filter : mFilterMap.values()) {
538             for (InputPort inputPort : filter.getConnectedInputPorts()) {
539                 inputPort.clear();
540             }
541             for (OutputPort outputPort : filter.getConnectedOutputPorts()) {
542                 outputPort.clear();
543             }
544         }
545     }
546 
getSubGraphs()547     Set<FilterGraph> getSubGraphs() {
548         return mSubGraphs;
549     }
550 
551     // Internal Methods ////////////////////////////////////////////////////////////////////////////
FilterGraph(MffContext context, FilterGraph parentGraph)552     private FilterGraph(MffContext context, FilterGraph parentGraph) {
553         mContext = context;
554         mContext.addGraph(this);
555         if (parentGraph != null) {
556             mParentGraph = parentGraph;
557             mParentGraph.mSubGraphs.add(this);
558         }
559     }
560 
assertNotRunning()561     private void assertNotRunning() {
562         if (isRunning()) {
563             throw new IllegalStateException("Attempting to modify running graph!");
564         }
565     }
566 }
567 
568