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
17package androidx.media.filterfw;
18
19import android.util.Log;
20import android.view.View;
21import androidx.media.filterpacks.base.BranchFilter;
22import androidx.media.filterpacks.base.FrameSlotSource;
23import androidx.media.filterpacks.base.FrameSlotTarget;
24import androidx.media.filterpacks.base.GraphInputSource;
25import androidx.media.filterpacks.base.GraphOutputTarget;
26import androidx.media.filterpacks.base.ValueTarget;
27import androidx.media.filterpacks.base.ValueTarget.ValueListener;
28import androidx.media.filterpacks.base.VariableSource;
29
30import java.util.Collection;
31import java.util.HashMap;
32import java.util.HashSet;
33import java.util.Map.Entry;
34import 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 */
46public 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         */
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         */
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         */
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
127        public FrameSlotSource addFrameSlotSource(String name, String slotName) {
128            FrameSlotSource filter = new FrameSlotSource(mContext, name, slotName);
129            addFilter(filter);
130            return filter;
131        }
132
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         */
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         */
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         */
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         */
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         */
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
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
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         */
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
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).
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
506    public void checkSignatures() {
507        checkSignaturesForFilters(mFilterMap.values());
508    }
509
510    // MFF Internal Methods ////////////////////////////////////////////////////////////////////////
511    Filter[] getAllFilters() {
512        return mAllFilters;
513    }
514
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     */
531    void wipe() {
532        mAllFilters = null;
533        mFilterMap = null;
534    }
535
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
547    Set<FilterGraph> getSubGraphs() {
548        return mSubGraphs;
549    }
550
551    // Internal Methods ////////////////////////////////////////////////////////////////////////////
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
561    private void assertNotRunning() {
562        if (isRunning()) {
563            throw new IllegalStateException("Attempting to modify running graph!");
564        }
565    }
566}
567
568